1 - CLI Scripts

Command-line maintenance scripts

The module ships CLI-only scripts. They are guarded: requested over HTTP (the modules_v4 directory is inside the web root) they answer 403 instead of running.

ScriptPurposeNeeds a webtrees install + DB?
cli/build-link-index.php [--limit=N] [--tree=<id>] [--rebuild] [--flush]build/update the link index for the XREF overviewyes
cli/build-uid-index.php [--limit=N] [--tree=<id>] [--rebuild] [--flush] [--force]build/update the UID index for UID lookupyes
cli/refresh-wthb-toc.php [--all] [--lang=<list>] [--clear] [--force]refresh the WTHB table of contents cache (production)yes
test-text-tag-collector.phpstandalone unit tests for the text-tag collectorno
test-link-classifier.phpstandalone unit tests for the link classifier (incl. target extraction)no
test-uid-tag-collector.phpstandalone unit tests for the UID tag collectorno
test-uid-index-service.phpstandalone unit tests for the UID index serviceno
tests/smoke-text-tag-collector.php [limit]scan records containing enhanced links, print every captured valueyes
smoke-migration5.phpSQLite smoke test for Migration5 (UID index schema)yes
tests/p1-measure.php [--tree=<id>]read-only scaling measurement for the XREF overviewyes

Run them from the webtrees root with the same PHP version the instance runs on (webtrees requires PHP 8.3+):

php modules_v4/linkenhancer/tests/test-text-tag-collector.php
php modules_v4/linkenhancer/tests/smoke-text-tag-collector.php 10

Cron example (every 2 hours, 500 records per run):

15 */2 * * * cd /path/to/webtrees && php modules_v4/linkenhancer/cli/<name>.php --limit=500 >> /var/log/linkenhancer-<name>.log 2>&1

2 - Contributing

How to contribute to the LinkEnhancer module

If you’d like to contribute to this module, great! You can contribute by:

  • Contributing code — check out the issues for things that need attention. If you have changes you want to make not listed in an issue, please create one, then you can link your pull request.
  • Testing — it’s all manual currently, please create an issue for any bugs you find.

Translation

You can contribute to this project on Codeberg Weblate. Updated translations will be included in the next release of this module.

Beside English the following languages are available:

  • Catalan (by Bernat Josep Banyuls i Sala)
  • Dutch (by TheDutchJewel)
  • Español (by Bernat Josep Banyuls i Sala)
  • German

Repository

Primary Repository: https://codeberg.org/bschwede/linkenhancer

All activity happens on Codeberg:

Mirror Repository: https://github.com/bschwede/linkenhancer

3 - Project Structure

Directory layout, dependencies and database schema

Directory layout

linkenhancer/
├── module.php            ← webtrees entry point
├── autoload.php          ← PSR-4 autoloader
├── cron-jobs.php         ← cronjob self-registration manifest
├── latest-version.txt    ← current version (synced to LinkEnhancerModule.php)
├── composer.json         ← PHP deps (wt-shared-libs, phpstan)
├── package.json          ← JS deps (gulp, rollup, mocha)
├── gulpfile.mjs          ← build tasks
├── phpstan.neon          ← static analysis config
├── phpstan-bootstrap.php ← PSR-4 bootstrap for PHPStan
├── phpstan-baseline.neon ← known issues
├── src/                  ← PHP (PSR-4)
│   ├── LinkEnhancerModule.php
│   ├── LinkEnhancerModuleExt.php
│   ├── LinkEnhancerUtils.php
│   ├── SettingInterface.php
│   ├── CommonMark/       ← CommonMark extensions
│   ├── DataFix/          ← data-fix handlers + dispatcher
│   ├── Factories/        ← CustomMarkdownFactory
│   ├── Http/             ← request handlers
│   ├── Schema/           ← Migrations + SeedHelpTable
│   └── Services/         ← business logic
├── resources/
│   ├── js/               ← JS source (src/) + build output (bundle-*)
│   ├── css/              ← CSS source + build output
│   ├── lang/             ← .pot / .po / .php (translations)
│   ├── views/            ← PHTML views
│   └── img/              ← screenshots
├── cli/                  ← CLI maintenance scripts
├── tests/                ← standalone + smoke tests
└── util/                 ← build/maintenance shell scripts

Dependencies

Database schema

The module owns five tables (prefix le_), created incrementally by src/Schema/Migration#.php. Every migration is additive and idempotent, so the schema converges on each module load.

le_route_help_map

Maps a webtrees route (path / handler / method) to a help entry — the backing store for context-sensitive manual help. Seeded by SeedHelpTable.

ColumnTypeNotes
idint, PK (auto)
pathvarchar(255)route path
handlervarchar(150)fully-qualified handler class
handler_keyvarchar(150), indexversion-neutral key (2.2.6 + 2.3 compatible)
methodvarchar(20)
extrasvarchar(60)
subcontextvarchar(250)
categoryvarchar(60)standard / custom module / generic …
orderint, default 10
urlvarchar(250)
updated_attimestamp, nullable

Index on (path, handler) and on handler_key.

le_record_scan

One row per scanned GEDCOM record, with an MD5 fingerprint of the record text — lets the CLI index diff incrementally.

ColumnTypeNotes
fileinttree id (part of PK)
xrefvarchar(20)(part of PK)
rectypevarchar(15)GEDCOM tag, e.g. INDI, OBJE (part of PK)
hashvarchar(32)MD5 of the record text
scanned_attimestamp, nullable

Primary key (file, xref, rectype).

One row per (link, target) found in a record — the foundation for the planned Backlink feature.

ColumnTypeNotes
idint, PK (auto)
fileinttree id
xrefvarchar(20)
rectypevarchar(15)
tag_pathvarchar(255)where the link sits in the record
link_classvarchar(10)xref / ext / pic / classic / other
tokentextthe raw link token
snippettext, nullabledisplay context captured at build time
target_xrefvarchar(20), nullableXREF link target
target_treevarchar(255), nullable
target_uidvarchar(255), nullable, indexUID link target (filled by a length heuristic)

Indexes on (file, xref, rectype), target_xref and target_uid.

le_uid_index

One row per UID tag found in a record (record-level and fact-level). No unique constraint — UIDs are not guaranteed to be unique.

ColumnTypeNotes
idint, PK (auto)
fileinttree id
xrefvarchar(20)
uidvarchar(255), indexverbatim UID value (no normalization)
rectypevarchar(15)
tag_pathvarchar(255)
hashvarchar(32)MD5 fingerprint (incremental change detection)

Indexes on uid and (file, xref, rectype).

le_index_meta

A single row (id = 1) holding the freshness state of both the link index and the UID index.

ColumnTypeNotes
idint, PK, default 1single row
last_runtimestamp, nullablelast complete link-index run
rowsint, default 0link-index row count
uid_last_runtimestamp, nullablelast complete UID-index run
uid_rowsint, default 0UID-index row count

4 - Class Hierarchy

Auto-generated class overview

Auto-generated by docs/tools/generate-class-hierarchy.php — do not edit manually.

Module

  • LinkEnhancerModule extends AbstractModule implements MiddlewareInterface, ModuleCustomInterface, ModuleGlobalInterface, ModuleConfigInterface, ModuleDataFixInterface, ModuleListInterface, ModuleTabInterface, SettingInterface — LinkEnhancerModule.php
  • SettingInterface — SettingInterface.php
  • LinkEnhancerUtils — LinkEnhancerUtils.php
  • LinkEnhancerModuleExt extends LinkEnhancerModule implements ModuleExtGlobalInterface — LinkEnhancerModuleExt.php

CommonMark (Extensions)

  • LeImageRenderer (final) implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface — CommonMark/LeImageRenderer.php
  • SmartPunctHelper — CommonMark/SmartPunctHelper.php
  • LeTableOfContentsRenderer (final) implements NodeRendererInterface, XmlNodeRendererInterface — CommonMark/LeTableOfContentsRenderer.php

Factories

  • CustomMarkdownFactory extends MarkdownFactory — Factories/CustomMarkdownFactory.php

Request Handlers

  • DataFixBlocksAction implements RequestHandlerInterface — Http/RequestHandlers/DataFixBlocksAction.php
  • HelpWthbAction implements RequestHandlerInterface — Http/RequestHandlers/HelpWthbAction.php
  • XrefDetailData (final) implements RequestHandlerInterface — Http/RequestHandlers/XrefDetailData.php
  • XrefOverviewListData (final) implements RequestHandlerInterface — Http/RequestHandlers/XrefOverviewListData.php
  • GotoIdTreeAction extends GotoIdAction — Http/RequestHandlers/GotoIdTreeAction.php
  • GotoUidTreeAction extends GotoUidAction — Http/RequestHandlers/GotoUidTreeAction.php
  • GotoXrefAction implements RequestHandlerInterface — Http/RequestHandlers/GotoXrefAction.php
  • DataFixRebuildIndexAction implements RequestHandlerInterface — Http/RequestHandlers/DataFixRebuildIndexAction.php
  • RenumberActionHandler (final) — Http/RequestHandlers/RenumberActionHandler.php
  • HelpMdAction implements RequestHandlerInterface — Http/RequestHandlers/HelpMdAction.php
  • AdminXrefOverviewData (final) implements RequestHandlerInterface — Http/RequestHandlers/AdminXrefOverviewData.php
  • GotoIdAction extends AbstractGotoAction — Http/RequestHandlers/GotoIdAction.php
  • HelpWtCoreAction implements RequestHandlerInterface — Http/RequestHandlers/HelpWtCoreAction.php
  • WthbAdminHandler (final) — Http/RequestHandlers/WthbAdminHandler.php
  • GotoUidAction extends AbstractGotoAction — Http/RequestHandlers/GotoUidAction.php
  • AbstractGotoAction (abstract) implements RequestHandlerInterface — Http/RequestHandlers/AbstractGotoAction.php

Schema

  • SeedHelpTable implements SeedInterface — Schema/SeedHelpTable.php
  • Migration6 implements MigrationInterface — Schema/Migration6.php
  • Migration2 implements MigrationInterface — Schema/Migration2.php
  • Migration7 implements MigrationInterface — Schema/Migration7.php
  • Migration3 implements MigrationInterface — Schema/Migration3.php
  • Migration4 implements MigrationInterface — Schema/Migration4.php
  • Migration1 implements MigrationInterface — Schema/Migration1.php
  • Migration5 implements MigrationInterface — Schema/Migration5.php
  • Migration0 implements MigrationInterface — Schema/Migration0.php

Services

  • AdminSettingsBuilder (final) — Services/AdminSettingsBuilder.php
  • MarkdownEditorActivationService (final) — Services/MarkdownEditorActivationService.php
  • IndexRebuildScheduler (final) — Services/IndexRebuildScheduler.php
  • UidTagCollector (final) — Services/UidTagCollector.php
  • TextTagCollector (final) — Services/TextTagCollector.php
  • ContentBuilder (final) — Services/ContentBuilder.php
  • RenumberWithLinksService (final) — Services/RenumberWithLinksService.php
  • DataFiles (final) — Services/DataFiles.php
  • XrefOverviewColumns (final) — Services/XrefOverviewColumns.php
  • LinkIndexService (final) — Services/LinkIndexService.php
  • XrefsService (final) — Services/XrefsService.php
  • XrefDetailService (final) — Services/XrefDetailService.php
  • WthbService — Services/WthbService.php
  • IdResolver (final) — Services/IdResolver.php
  • LinkRenderer (final) — Services/LinkRenderer.php
  • WthbTocService (final) — Services/WthbTocService.php
  • UidIndexService (final) — Services/UidIndexService.php

DataFix

  • FixHandlerInterface — DataFix/FixHandlerInterface.php
  • DataFixDispatcher — DataFix/DataFixDispatcher.php
  • TreeLookupTrait — DataFix/TreeLookupTrait.php
  • DanglingLinksFix (final) implements FixHandlerInterface — DataFix/Handlers/DanglingLinksFix.php
  • DuplicateUidFix (final) implements FixHandlerInterface — DataFix/Handlers/DuplicateUidFix.php
  • WtTypeFix (final) implements FixHandlerInterface — DataFix/Handlers/WtTypeFix.php
  • XrefUidSwapFix (final) implements FixHandlerInterface — DataFix/Handlers/XrefUidSwapFix.php
  • AmbiguousUidFix (final) implements FixHandlerInterface — DataFix/Handlers/AmbiguousUidFix.php

5 - Development Environment

Setting up a local development environment

What you need to build and test the module locally.

Requirements

ComponentVersionNotes
PHP8.3+same version the webtrees instance runs on
Composer2.xinstalls wt-shared-libs + phpstan
Node.js18+ (LTS)for Gulp / Rollup / Mocha
webtrees2.2.x or 2.3.xneeded for PHPStan (app/ classes) and the smoke tests
wt-shared-libsnewest versionsibling repo at modules_v4/wt-shared-libs (composer path repo)

Location

The module lives inside a webtrees installation at modules_v4/linkenhancer/. Its composer.json pulls in the sibling shared library as a local path repository:

"repositories": [
    { "type": "path", "url": "../wt-shared-libs", "options": { "symlink": false } }
]

Setup

# PHP dependencies (installs wt-shared-libs + phpstan)
composer install

# JS dependencies (gulp, rollup, mocha, …)
npm ci

Note: composer.json sets prepend-autoloader: false — the module’s autoloader is attached to (not replacing) webtrees’ autoloader.

Bind mount

In the current setup, development is being done on Linux with the Apache web server installed natively.The source tree is exposed to the PHP runtime through a bind mount of the rpo folders into modules_v4. Run the CLI tools and PHPStan from the mount path (the path the webtrees instance sees), not the source path, so app/ and vendor/autoload.php resolve:

cd /path/to/webtrees/modules_v4/linkenhancer
composer analyse

See the CLI Scripts and PHPStan pages for the exact commands.

6 - Build System

Gulp tasks for JS/CSS bundling, translations and versioning

Bundling, translation compilation and versioning are driven by Gulp tasks in gulpfile.mjs , invoked with npx gulp <task>. The npm run * scripts cover testing only — see the Testing page.

Tasks

TaskCommandWhat it does
buildnpx gulp buildclean → JS (Rollup) + CSS (PostCSS)
devbuildnpx gulp devbuildlike build, but Dev-Mode (no minify, source maps)
updateponpx gulp updateporuns update-po-files.sh → xgettext over src/ → messages.pot
po2phpnpx gulp po2phpcompiles resources/lang/*.po → resources/lang/*.php
syncversionnpx gulp syncversionkeeps latest-version.txt ↔ LinkEnhancerModule.php in sync
bumpversionnpx gulp bumpversionrequires a clean git tree → version bump → syncversion
archivenpx gulp archivebuild + po2php + create-archive.sh (release ZIP)
forgejo:issue-refsnpx gulp forgejo:issue-refs[:dry]adds issue refs to commit messages (needs FORGEJO_TOKEN in .env) - was used only once to migrate the repo from GitHub to Codeberg

JS / CSS bundling

build = clean → (in parallel) JS + CSS.

  • clean deletes resources/js/bundle* and resources/css/bundle* first.
  • JS (Rollup): two entry bundles — jsIndex (the main index-le.js ) and jsExtractLeConfig (le-config.js ). Output to resources/js/bundle*.
  • CSS (PostCSS): per-file subtasks from getCssSubTasks(), with autoprefixer + cssnano. Output to resources/css/bundle*.

Dev-Mode (devbuild) toggles a flag via toggleDevMode(true) — it disables minification and emits source maps.

Build & archive pipeline

flowchart LR
    subgraph B["🔧 npx gulp build"]
        C["🧹 clean<br/>(bundle*)"]
        J["`📜 Rollup<br/>**index-le.js** · le-config.js`"]
        P["🎨 PostCSS<br/>autoprefixer · cssnano"]
        C --> J
        C --> P
    end
    subgraph A["📦 npx gulp archive"]
        A1["build"]
        A2["po2php"]
        A3["`create-archive.sh<br/>**dist/linkenhancer_v<ver>.zip**`"]
        A1 --> A2 --> A3
    end

    classDef bStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
    classDef aStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px

    class C,J,P bStyle
    class A1,A2,A3 aStyle

Translation files

  • updatepo only regenerates the template (messages.pot). The .po files themselves are updated via Weblate, not locally, to avoid conflicts — see the Translation Pipeline page.
  • po2php converts the committed .po files into the PHP arrays webtrees loads at runtime.

7 - Translation Pipeline

How translatable strings flow from source to runtime (xgettext → Weblate → PO → PHP)

The module ships its own translations through the standard gettext chain, hosted on Codeberg Weblate.

Flow

flowchart TB
    subgraph Extraction["1️⃣ Code → POT"]
        direction LR
        PHP["📝 src/*.php"] -->|npx gulp updatepo| POT["📄 messages.pot"]
    end

    subgraph Translation["2️⃣ Translation"]
        direction LR
        WEBLATE["🌐 Weblate"] -->|download| PO["🗝 <lang>.po"]
    end

    subgraph Rendering["3️⃣ Webtrees"]
        direction LR
        PHPARR["📦 lang.php"] -->|I18N| RENDER["✅ Render"]
    end

    POT -->|upload| Translation
    PO -->|npx gulp po2php| Rendering

    classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
    classDef fileStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
    classDef weblateStyle fill:#f3e5f5,stroke:#7B1FA2,stroke-width:2px
    classDef renderStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px

    class PHP sourceStyle
    class POT,PO,PHPARR fileStyle
    class WEBLATE weblateStyle
    class RENDER renderStyle

Extracting strings

update-po-files.sh runs xgettext over every *.php / *.phtml except util/, vendor/, node_modules/ and tests/:

xgettext -L PHP \
  --keyword=translate --keyword=plural:1,2 --keyword=translateContext:1c,2 \
  --add-comments=I18N --from-code=utf-8 \
  --output=resources/lang/messages.pot …

Two marker helpers control what gets extracted:

  • I18N::translate() / MoreI18N::translate() — module-specific strings. translate is an xgettext keyword, so these are picked up.
  • MoreI18N::xlate() — strings already covered by the webtrees core POT. Same runtime behaviour, different name, therefore invisible to xgettext. After a core update, re-compare messages.pot with the core POT and switch any newly-covered msgid to xlate().

Add /* I18N: … */ comments to give translators context (picked up via --add-comments=I18N).

PO → PHP

.po files are not merged locally — that is Weblate’s job, to prevent conflicts. Once Weblate delivers updated .po files, compile them:

npx gulp po2php

Note: a former .po → .mo step is obsolete (translation format switch, issue #88).

8 - WTHB TOC Language Pipeline

How the German webtrees manual TOC is produced (GenWiki → pywikibot → generation → external repo / Pages / Weblate → module)

Overview

The linkenhancer-wthb-toc-lang package delivers the table of contents (TOC) of the German webtrees manual — and its translations — to the WTHB help feature. Its production is a round trip that lives entirely outside the webtrees install: it reads the manual from GenWiki, regenerates the TOC, writes the result back to GenWiki as a MediaWiki template, and — in parallel — ships the structured data to an external Codeberg repo that feeds Codeberg Pages, Weblate (translations) and, finally, the module. This page documents the production side; the consumer side (cache, CLI, rendering) is covered on the WTHB feature page and the CLI scripts page.

Ressources

Pipeline

The round trip has two halves: the GenWiki round trip (read the manual → generate → write the TOC template back) and the delivery pipeline (structured data → external repo → Pages / Weblate → module).

The GenWiki round trip:

flowchart TB
    subgraph HOST["🖥️ Host · gw-skripte/"]
        ORCH["`🚦 **wt-maintain.sh**<br/>*Orchestration*`"]
        PREAD["`🤖 **pywikibot** · *wt-maintain.py*<br/>SectionsExportBot — *READ*`"]
        GEN["`⚙️ **wt-gen-toc.py** *(pandas)*`"]
        PREP["`📄 *pagefromfile file*<br/>vorlage_wt-verzeichnis-toc.txt`"]
        PWRITE["`🤖 **pywikibot** · *pagefromfile*<br/>*WRITE* — Bot-Summary`"]
    end

    GW["`📖 **GenWiki** · wiki.genealogy.net<br/>*webtrees handbook pages*`"]
    CSV["`📄 *3 CSV*<br/>pageinfo · sections · images`"]
    OUT["`📦 *Outputs*<br/>wthb-toc.wiki · .html · .json · de.json`"]
    TPL["`📖 **Vorlage:WT-Verzeichnis-TOC**<br/>*noinclude + includeonly TOC*`"]
    API["`🔍 *API action=parse* → HTML<br/>(curl + jq)`"]

    GW -->|"allpages prefix 'Webtrees Handbuch'<br/>API parse/sections + imagelinks"| PREAD
    ORCH -.->|"drives"| PREAD
    PREAD --> CSV
    CSV -->|"+ wthb-pageinfo_ext.csv (target)"| GEN
    GEN --> OUT
    OUT --> PREP
    PREP --> PWRITE
    PWRITE -->|"pagefromfile -force"| TPL
    TPL -->|"curl API parse"| API

    classDef srcStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef coreStyle fill:#ede7f6,stroke:#5e35b1,stroke-width:2px
    classDef okStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    classDef pwbStyle fill:#fff8e1,stroke:#f57f17,stroke-width:2px
    classDef targetStyle fill:#fce4ec,stroke:#ad1457,stroke-width:2px

    class GW srcStyle
    class ORCH,GEN coreStyle
    class CSV,OUT,API okStyle
    class PREAD,PWRITE pwbStyle
    class TPL targetStyle

Orchestration — wt-maintain.sh

Everything runs from one host script (in gw-skripte/):

wt-maintain.sh <wthb-pageinfo_ext.csv> <DATADIR>

It depends on setenv.sh exporting PWB_PY, PWB_CONF and SCRIPTDIR, and drives these steps in order:

  1. query the GenWiki data (pywikibot READ)
  2. generate the TOC
  3. build the pagefromfile wrapper file
  4. write the template back to GenWiki (pywikibot WRITE)
  5. parse-check the rendered template (curl + jq)
  6. deploy the JSON to the external repo
  7. downstream: repo CI, Weblate and the module pick the data up

GenWiki data query (pywikibot READ)

Step 1 is wt-maintain.py, a pywikibot bot (SectionsExportBot, extending ExistingPageBot). It calls site.login(), iterates allpages(prefix="Webtrees Handbuch") and, per page, queries the MediaWiki API (action=parse&prop=sections) plus imagelinks() (FilePage info).
The result is three CSV files:

  1. wthb-pageinfo.csv,
  2. wthb-sections.csv and
  3. wthb-images.csv.

Two manual follow-ups remain (per the script header): (b) maintaining the image directory and (c) adding the images to the Kategorie:Webtrees category — done by hand (helper: wt-webtrees-cat-files.sh).

TOC generation — wt-gen-toc.py

Step 2 is a pandas script that combines the IST state (the three CSVs) with the SOLL target structure in wthb-pageinfo_ext.csv. classify_prefix() assigns chapter/section prefixes. It writes four outputs:

  • wthb-toc.wiki — the MediaWiki TOC source
  • wthb-toc.json — the structured data the module consumes
  • de.json — the German language file for Weblate
  • wthb-toc.html — a rendered HTML copy (legacy/parallel — the current consumer renders from JSON)

GenWiki template (pywikibot WRITE)

Steps 3–4 wrap the generated TOC in the MediaWiki template Vorlage:WT-Verzeichnis-TOC and write it back:

  • the pagefromfile file combines a <noinclude> part ({{Dokumentation}} + [[Kategorie:Webtrees]]) with an <includeonly> part (Stand: <datum> + <div class="wthbtoc">…</div>).
  • pywikibot pagefromfile -force -pt:1 -summary:"Bot: Webtrees Handbuch Inhaltsverzeichnis aktualisiert" performs the write.

Step 5 verifies the result by parsing it back through the GenWiki API (action=parse&format=json) and extracting the HTML with jq -r '.parse.text["*"]'.

Note: this is automatic writing to GenWiki via pywikibot — the opposite of the “produce review output, no bot” approach used for the GenWiki help-docs export. Vorlage:WT-Verzeichnis-TOC and Kategorie:Webtrees are GenWiki-specific German names and are kept verbatim.

External repo, CI & Pages

The second half of the pipeline (step 6 onward) ships the structured data out of the host:

flowchart TB
    OUT["`📦 *Outputs*<br/>wthb-toc.json + de.json`"]
    DEP["`🚀 **wt-deploy-toc-lang.sh**`"]

    subgraph EXTERNAL["🌐 External infrastructure"]
        REPO["`📦 **linkenhancer-wthb-toc-lang**<br/>(Codeberg)`"]
        META["`🔧 *tools/gen_meta.py*<br/>languages.json + version.json`"]
        CI["`⚙️ *CI regen-meta.yml*<br/>gen_toc_html.py`"]
        PG["`🌐 **Codeberg Pages**`"]
        WLB["`🌐 **Weblate** · linkenhancer/wthb-toc`"]
        TNG["🗝 en.json · fr.json · …"]
    end

    subgraph MODULE["📦 Module (linkenhancer)"]
        SVC["`**WthbTocService**`"]
        DS["`💾 *data/…/wthb-toc-lang/*<br/>2-Layer-Cache`"]
        RDR["`📄 **help-wthb-toc.phtml**<br/>TOC-Modal`"]
    end

    OUT -->|"cp wthb-toc.json + de.json"| DEP
    DEP -->|"git add / commit / push"| REPO
    REPO -->|"regen meta"| META
    REPO --> CI --> PG
    REPO -->|"de.json (Source)"| WLB
    WLB --> TNG
    TNG -->|"[lang].json"| REPO
    PG -->|"fetch JSON"| MODULE
    SVC --> DS --> RDR

    classDef coreStyle fill:#ede7f6,stroke:#5e35b1,stroke-width:2px
    classDef okStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    classDef queueStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px

    class OUT okStyle
    class DEP,REPO,META,CI,SVC coreStyle
    class PG,TNG,DS,RDR okStyle
    class WLB queueStyle
  • wt-deploy-toc-lang.sh copies wthb-toc.json and de.json into the linkenhancer-wthb-toc-lang repo, runs tools/gen_meta.py (regenerates languages.json + version.json) and does git add/commit/push. It is standalone-callable and soft-skips (warning + exit 0) when the repo or JSON is missing, so it never aborts the set -e orchestrator.
  • Repo CI (regen-meta.yml) runs gen_toc_html.py and publishes to Codeberg Pages.

Weblate

The repo’s de.json is the source of the Weblate component wt-modules/linkenhancer-wthb-toc; the other [lang].json files are its translation targets. Translations flow back into the repo and are served via Codeberg Pages.

Consumer (module)

Briefly — see the WTHB feature page and the CLI scripts page for details:

Configuration & prerequisites

  • setenv.sh — exports PWB_PY, PWB_CONF, SCRIPTDIR.
  • pywikibot config — user-config-test.py / user-config-prod.py.
  • wthb-toc-lang.env — TOC_LANG_REPO=$HOME/dev/linkenhancer-wthb-toc-lang, TOC_LANG_DATADIR=$HOME/dev/wthb-toc-datadir.
  • Prerequisites: pywikibot, pandas, jq, python3.

Ops & caveats

  • Manual image follow-up — the image directory and Kategorie:Webtrees must be maintained by hand.
  • pagefromfile login — the script comment notes that site.login() may need to be inserted into pagefromfile.py.
  • Soft-skip deploy — a missing repo or JSON is a warning, not a failure (keeps set -e safe).

9 - Testing

Test chain: PHP standalone, PHPStan, and JS (Mocha)

The full chain runs with:

npm test

which executes test:php → analyse:php → test:js, each step required to pass before the next runs.

JavaScript (Mocha + jsdom)

npm run test:js
# mocha 'resources/js/tests/**/*.test.js' --require jsdom-global/register

PHP standalone unit tests

npm run test:php
# runs every tests/test-*.php with the system PHP — no webtrees install, no DB
TestCovers
test-text-tag-collector.phptext-tag collector
test-link-classifier.phplink classifier (incl. target extraction)
test-uid-tag-collector.phpUID tag collector
test-uid-index-service.phpUID index service
test-i18n-mark.phpi18n markers
test-wthb-toc-service.phpWTHB TOC service

PHP static analysis

npm run analyse:php      # or: composer analyse
# PHPStan Level 6 — see the PHPStan page

Smoke tests (need webtrees + DB)

These exercise a real installation and are not part of npm test:

php tests/smoke-text-tag-collector.php [limit]
php tests/smoke-migration5.php
php tests/p1-measure.php [--tree=<id>]

10 - PHPStan

Static analysis setup and configuration

Required: PHPstan

Static analysis is configured via phpstan.neon (Level 6, baseline in phpstan-baseline.neon).

Run

composer analyse

The analysis requires a webtrees installation (for app/ classes + vendor/autoload.php). Run it from the bind-mounted path:

cd /path/to/webtrees/modules_v4/linkenhancer
composer analyse

Configuration

  • Level: 6
  • Baseline: phpstan-baseline.neon (known issues)
  • Bootstrap: phpstan-bootstrap.php registers PSR-4 namespaces directly (bypasses the version-gated autoloader in wt-shared-libs)
  • Scan directories: ../../app/ (webtrees core classes)
  • Exclude paths: LinkEnhancerModuleExt.php (use in conjunction with Vesta modules)
  • Memory limit: 1G
  • Parallel: disabled (single process, more reliable in CI)

Key settings

  • treatPhpDocTypesAsCertain: false — prevents false positives from @var assertions
  • ignoreErrors for:
    • AccessLevel Enum (WT 2.3-only class, not available in 2.2 runtime)
    • DB::prefix('') — intentional (getting prefix without identifier)

Regenerating the baseline

After intentional changes that introduce new (acceptable) errors:

vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G --generate-baseline=phpstan-baseline.neon

Note: Run this via the direct vendor/bin/phpstan call — the composer script wrapper does not forward extra arguments.

11 - Releasing

Version bump, build, and release archive creation

Steps

# 1. Bump the version (requires a clean git tree)
npx gulp bumpversion

# 2. Build + compile translations + create the release archive
npx gulp archive

What archive does

npx gulp archive runs build → po2php → create-archive.sh . The script:

  1. Reads the version from latest-version.txt .
  2. rsyncs the module into dist/linkenhancer/, excluding everything that is not shipped (below).
  3. Zips it to dist/linkenhancer_v<version>.zip.

Excluded from the archive

create-archive.sh drops, among others: node_modules/, .git*, gulpfile.mjs, package*.json, phpstan*, tests/, the util/* build scripts, resources/js/src, resources/js/tests, resources/img/, resources/lang/*.po / *.mo, and .env. Only the built bundle* JS/CSS, the compiled resources/lang/*.php, src/, resources/views/, resources/css/ and the module entry files are shipped.

Publishing

  1. Tag the release in git.
  2. Upload dist/linkenhancer_v<version>.zip to a Codeberg / GitHub release with release notes.

The archive is not committed to the repo — it is produced on demand and attached to the release.