Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[1.34.0]¶
Upgrade: no database changes; cache:flush is enough.
Fixed¶
- Widget and read-aloud kept the language they were loaded in. Both read
<html lang>once. A DOM translation (nt-lingua) setslang/dirat runtime, so the widget stayed German, languages other than de/en fell back to German, and read-aloud spoke the translated text with the voice of the source language. Both now watchlanganddiron<html>with a MutationObserver — deliberately not an nt-lingua event, so any translator works: - The widget sets its texts again (panel, launcher label, tooltip, hints) without rebuilding itself and without touching the stored settings; an open panel stays open and keyboard focus stays on its control.
- With
dir="rtl"the panel is mirrored (containerdir, logical CSS alignment); the launcher keeps its configured corner. - Read-aloud updates its language, chooses the voice again and stops a
running playback (
nt-tts:stopwithreason: language).
Added¶
- Static widget and read-aloud texts for fr, it, es, pt, nl, pl, ro, hr, sr (Cyrillic), bs, sq, el, tr, uk, ru and ar. Unknown languages now get English instead of German. The form of address (Sie/du) still applies to German only; the site's own intro and tooltip texts apply to the language the page was delivered in.
window.ntA11y.getLang(),window.ntTts.getLang()and the eventnt-a11y:language.- Browser test
Tests/JavaScript/widget-language.cjs(Playwright).
[1.33.0]¶
Upgrade: new table tx_ntai_pdf_task. Run cache:flush, then
extension:setup (or Analyze Database Structure). The PDF analyzer version
rises to 9, so the next nt_ai:pdf-audit run audits every PDF again.
Added¶
- PDF work list: priority and measure. Most of a grown PDF stock is not repaired but deleted, rebuilt as a web page or exported anew from its source. The PDF module now records that decision per file:
- Priority in three plain tiers, computed on display: high = errors and publicly linked, medium = errors without a public link or warnings / "not auditable" with a public link, low = other findings.
- Measure with a note in the findings view: open, delete/archive, as a web page, re-export from source, remediate, exception, done. Stored in its own table, so it survives re-audits and the replacement of the file; removed when the file is deleted.
- Filters by priority and measure, sorting by priority (most-linked first within a tier), a tile "Ohne Maßnahme" and a hint at files that are not linked anywhere.
- The CSV export carries priority, measure and note.
- PDF quick fix: title, language, title display. The findings view offers
to set a missing or meaningless document title, a missing document language
and "show title in window" (
ViewerPreferences/DisplayDocTitle): - Suggestions from the existing values, the first meaningful line of text or
the file name; optionally an AI title (subject to
pdf.altTextDataPolicy). Nothing is written without the editor's confirmation. - Written as an incremental update in pure PHP: the original bytes stay untouched, only the changed objects and a new cross-reference section are appended, in the form the file already uses (classic table, stream or hybrid). Objects in object streams get a new number. The title is set in the document information and in the XMP metadata.
- The corrected copy is read back and must show the new values with
unchanged pages, tags, figures, form fields and fonts; the new setting
pdf.verifyCommandadds an external check (e.g.qpdf --check, or pikepdf with the bundledResources/Private/Scripts/pdf-check.py); it refuses a copy that fails where the original passes. Only then is the file replaced via FAL (same uid, links kept) and audited again. - The original is kept in
var/nt_ai/pdf-original/; one click restores it. - Encrypted and digitally signed PDFs are refused. A PDF/UA identifier is never set.
- Bulk fix: language (where missing) and title display for up to ten PDFs with a usable title per run.
- Exceptions in the accessibility statement. PDFs marked as exception
are passed to the statement draft (
pdf_exceptions) and listed under "Non-accessible content" with the editors' note as the reason; the "Erklärung" tab shows them before generating. Files from non-public storages are left out, because the statement is public and the name of a protected document is not.
Fixed¶
- Title of Word PDFs reported missing. For hybrid-reference files
(Microsoft Office) the analyzer chains a copy of the cross-reference stream
in front of the classic table (since 1.28.0). The PDF parser takes the
trailer from that copy, which names neither catalog nor document
information, so the title of such files was not found and "no document
title" was reported. The copy now carries
/Root,/Infoand/IDof the classic trailer. - Catalog after an incremental update. The analyzer read the first object of type Catalog; after an update a file can hold an older one, and the PDF parser lets an object stream override the newer version. The catalog is now the one the trailer names.
[1.32.1]¶
Upgrade: no database changes; cache:flush is enough.
Fixed¶
- Accessibility widget outside any landmark. The widget container was
appended to
bodynext toheader,mainandfooterwithout a role, so screen-reader users navigating by landmarks did not find it, and checkers reported "landmarks/region" (best practice for WCAG 1.3.1) on every page. It is now a named region (role="region",aria-label= panel title). Adivwith a role rather than<aside>, so theme styles forasidedo not apply. Behaviour and appearance are unchanged. Reported by Stadt Villingen-Schwenningen.
[1.32.0]¶
Upgrade: no database changes; cache:flush is enough. Then run
vendor/bin/typo3 nt_ai:reference-text-repair --list and, if it finds
anything, --apply (see below).
Two findings reported by Stadt Villingen-Schwenningen.
Fixed¶
- Image usages hid the file's alt text and description. On
sys_file_reference,alternativeanddescriptionare nullable with modeuseOrOverridePlaceholder: NULL means "use the file's text", an empty string "deliberately no text". The AI fields (ntAiAltText,ntAiImageDescription) rendered the file's text as placeholder but not the core switch "Override default value", and always submitted the field — so every save of a content element with an expanded image stored''. The frontend then showedalt=""while editors still saw the grey text. Both elements now render the core switch exactly likeTextElement; switched off, NULL is stored. The AI button turns the switch on, as a generated text is meant for that usage. - Reports of pages the audit no longer checks stayed visible. A page turned
into a shortcut (or hidden, deleted, past its end time) drops out of the
scheduled audit, but its last report appeared in site overview, overall
report and declaration as the current state until the retention period
ended.
ReportRepository::findAllLatestPerPage()now applies the same page conditions as the audit task; the reports themselves are kept.
Added¶
- Command
nt_ai:reference-text-repairfor installations that ran 1.31.0 or earlier: lists image usages stored with'', and with--applyresets those to NULL whose file has a text (there''visibly hides it).--include-without-file-textalso resets the others,--fieldlimits it toalternativeordescription,--listshows each usage.
[1.31.0]¶
Upgrade: no database changes; cache:flush is enough, extension:setup
is not required.
Follow-up to 1.30.0: nine gaps reported by Stadt Villingen-Schwenningen after moving ten sites to the new frontend API.
Added¶
ntai.a11yWidget.hideMedia: restrictshideWhento a viewport (media query, e.g.(max-width: 767px)); withouthideWhenit hides the widget at that viewport alone. Re-evaluated when the viewport changes.nt-tts:clickmodeevent (detail.on) when "click to read" is switched on or off.window.ntA11y.unregisterToggle(id);registerToggle()now returns a function that removes the entry again.- CSS custom properties
--nt-a11y-launcher-hover/--nt-a11y-launcher-hover-dark(launcher background on hover and focus),--nt-a11y-title-color/--nt-a11y-title-color-dark(panel title),--nt-a11y-primary-dark-contrast(text on the dark "on" mark). - "Updating" section in the installation chapter: when
extension:setupis needed and when not; changelog entries now start with an Upgrade line.
Fixed¶
- Launcher hover/focus background was a fixed light blue (
#f4f7ff/#eef3ff) instead of following the primary colour. - Dark chrome: the "on" mark and the intensity sliders used the light primary
colour; with dark brand colours the mark on the active row dropped to about
1.4:1. They now use
--nt-a11y-primary-dark. - "Highlight focus" on a page in dark mode (
html[data-nt-theme="dark"]) now uses--nt-a11y-focus-dark. - The panel title inherited the site's
h2colour (and font settings), which could lack contrast on the panel background. nt-a11y:open/closefired again when the panel already was in that state (e.g.ntA11y.open()on an open panel), which also pulled the focus back to the close button.
[1.30.0]¶
Frontend: theming, events and read-aloud improvements requested by a user (Stadt Villingen-Schwenningen, overall feedback points A1, A2, B1, B4, B5, B6, F1, F2, F3). New site settings, no database changes. A new chapter "Frontend API" documents what themes may rely on.
Added¶
- Theming through CSS custom properties (B1). All fixed widget colours
(
#1c5bd8in 15 places, the dark-mode blues) are custom properties with the previous values as default:--nt-a11y-primary,--nt-a11y-primary-dark, derived tint and text colours,--nt-a11y-focus,--nt-tts-highlight. Site settingsntai.a11yWidget.primaryColor/primaryColorDarkset them per site. Tint and text on the tint follow the primary colour (color-mix). - Hiding and stacking (B4):
ntai.a11yWidget.hideWhen(selector; the widget hides while it matches),window.ntA11y.hide()/show(),--nt-a11y-z.--nt-a11y-offset-x/-yare documented as override points on.nt-a11y-widget, also in media queries. - Formal address by default (B5): German texts use "Sie";
ntai.a11yWidget.address = duswitches back.introText/tooltipTextreplace the two texts per site. - Widget events and API (F2):
nt-a11y:ready,open,close,change{ key, value, previous },reset;window.ntA11ywithopen,close,toggle,getPrefs,setPref,hide,show,refreshandregisterToggle()for own entries such as "Einfache Sprache" (B6). - Read-aloud events (F1):
nt-tts:beforecollect(cancelable, withwaitUntil(promise)),startwith its source (button, widget, click, api),segment,pause,resume,stopwith reason,fallback,error. The[data-nt-tts]button now goes through the publicwindow.ntTts.toggle, so a wrapper sees it.window.ntTts.wire()connects buttons added later. - Image text alternatives are read (A1) at their place as "Bild: …" /
"Image: …"; decorative images, file names and alt texts already in the
caption or surrounding text are skipped. The page-text check of the cloud
voice accepts these passages when the alt text is on the page. Based on a
patch contributed by the reporter. Setting
ntai.tts.readImageAlt(on). - Collapsed content is read (A2): Bootstrap
.collapse,<details>andhidden="until-found"in the reading area are opened before reading and restored on stop. Settingntai.tts.expandCollapsed(on).[data-nt-tts-skip]excludes an element from reading.
Not included¶
Texts from language files (only the two texts are configurable), dark-mode
option colorScheme (B2), focus safety-net options (B7), highlight timed to
the audio and preloading (A3), the refusal reason (A4), exclusion settings
beyond data-nt-tts-skip (A5), a read-aloud Fluid partial (A6).
Checked in a browser on a development site with a stand-in voice: formal texts, colour from the custom property, all events, API incl. an own entry, hiding, an image read at its place, accordion and collapse read and closed again; light and dark panel unchanged in appearance.
[1.29.0]¶
Second user review (stock of 4,345 PDFs, overall feedback of 41 points): the
PDF follow-ups and the small items. All PDF reports are rebuilt on the next
nt_ai:pdf-audit run (analyzer version 8). No database changes.
Changed¶
- Usages count what an anonymous visitor can reach now. Start and stop
times of the linking record and its page, frontend user groups (except "hide
at login") and parent pages that pass hidden/time/group on to subpages
(
extendToSubpages) are evaluated. Scheduled versions of one element count once, attachments of expired news not at all. Measured by the reporter: 1,647 "publicly linked" against 982 in their own inventory. - "Nicht prüfbar" (grey) instead of yellow or red for PDFs the check could not open: encrypted with access for assistive technology allowed, or not auditable (time limit, size, parse error). Shown in the module (own total), the file list, the metadata notice, the CSV and the e-mail report; they no longer count as "with errors". Encrypted with access forbidden stays red.
- Upload notice for editors without access to the PDF module points to the metadata of the file instead of the module.
- Defaults for alt texts on upload:
altText.generateOnUploadSourceis nowbackend(only real backend uploads; opening a folder that indexes new files cost API calls before),altText.generateOnUploadLimitis 20. Stored values are kept. nt_ai:generate-alt-textscan be scheduled (still at most--limitfiles per run, default 50) and only sends JPEG, PNG, GIF and WebP — TIFF, PSD, BMP, EPS and HEIC were sent and rejected with HTTP 400.- Reading scope falls back to
main:ntai.tts.scopetakes a list, the first selector present wins; default#maincontent, main, [role=main]. Themes without#maincontentread the main content, not the whole body.
Added¶
- More unusable titles: program defaults in brackets, a single word with any extension ("Kernmacherei-19.6.1.mx", "Normal.dot"), file names with underscores, printer/scanner job names (no space, at least half digits), template placeholders ("Titel", "Title"), and a title equal to the file name (compared without extension, case and separators).
- Retention: the log tables (
tx_ntai_audit_reportperaudit.retentionDays, token usage 400 days, page scores and history 365, Lighthouse 180) are offered to the core task "Table garbage collection"; the task can override the periods.audit.retentionDayshad been read but never applied.
Fixed¶
- Token usage never recorded the file. Every alt-text, PDF alt-text and
image-description call stored
file_uid = 0, so costs could not be attributed to files. All callers now pass the file. - Rate-limit buckets stored a plain SHA-1 of the IP, reversible for IPv4 in seconds. Now keyed with the encryption key (HMAC); existing buckets simply expire.
- Colour-deficiency switch showed "aus" in lower case while every other switch shows "AUS".
Documentation¶
- Usages: only fields the reference index knows are found (custom TCA needs a
soft reference parser). Large map collections: plan them per folder with
--folder.
[1.28.0]¶
Fixed¶
- Tagged PDFs from Microsoft Office were reported as "not tagged". Office
writes hybrid-reference files: a classic cross-reference table plus a
cross-reference stream that the trailer names only by
/XRefStm. The structure tree and its elements sit in object streams listed solely in that stream. smalot/pdfparser does not read/XRefStm(neither 2.12 nor 2.13), so/StructTreeRootresolved to nothing. A user measured 1,429 of 2,987 "not tagged" findings in a stock of 4,345 PDFs to be this case. The analyzer now appends, in memory, a copy of that stream chained in front of the classic sections, so the parser reads both; the file on disk is not changed. On 25 real Office PDFs (Word, PowerPoint, Publisher 2010–2019) all structure trees are found now, none before; pages and text are unchanged. As the tree becomes visible, these PDFs are checked by the tag rules for the first time: figures without alternative text and the otherpdf.structurefindings can appear where "not tagged" stood before. All PDF reports are rebuilt on the nextnt_ai:pdf-auditrun (analyzer version 7). A test keeps watching the parser: once it reads/XRefStmitself, the workaround can go.
[1.27.1]¶
Fixed¶
- The "default provider is not configured" error no longer recommends Ollama as usable. Ollama counts as available as soon as a URL is set, and one is set by default; whether its service runs is not checked. The message introduced in 1.22.3 therefore told a fresh installation without any API key "Usable right now: ollama" — following that advice ended in a connection error. Providers with credentials are now listed on their own; Ollama is mentioned separately with the note that its service is not checked. With only Ollama configured the message says that no provider has credentials yet. Found by an automated review of the 1.22.3 pull request.
[1.27.0]¶
PDF accessibility check, fifth set from the user review: large batches from
the module and encrypted PDFs. No database changes. All PDF reports are
rebuilt on the next nt_ai:pdf-audit run (analyzer version 6).
Changed¶
- The module's "audit all new and changed" no longer runs thousands of files
inside one backend request. Up to ten outdated PDFs are audited at once as
before; more are queued, one job per file (type
pdf_audit), and worked off bynt_ai:process-queue. The module shows how many are waiting; a file is queued only once; a file audited in the meantime is skipped; a file that cannot be audited is recorded as such instead of being retried. The scheduler tasknt_ai:process-queuehas to exist — without it the jobs stay queued. - Encrypted PDFs are told apart by their permission flags. Until now every encrypted PDF got the same error ("cannot be analysed"). The flags are now read from the encryption dictionary: text extraction for assistive technology forbidden — error; allowed (copy or change protection only) — warning that the file is otherwise unchecked. If the flags cannot be read, the previous error stays. Revision 2 has no accessibility bit; there the copy bit decides.
- A PDF that could not be opened scores 49 at most, whatever its findings: one warning would otherwise leave an unexamined file at 97/100.
Not included¶
Whether a password is required to open an encrypted file. The module has no live progress bar; the count updates on reload.
Checked with hand-built PDFs for every case, with two real encrypted PDFs (flags read, warning), and on a development site with 13 outdated PDFs: queued by one click, no duplicates on a second click, processed by the queue worker.
[1.26.0]¶
PDF accessibility check, fourth set from the user review: getting the results to the people who maintain the files. No database changes, no new settings.
Added¶
- CSV export in the PDF module: the list as shown (filter, sorting) with
status, score, counts, usages, date and the finding texts. Semicolon and BOM
for Excel; cell values go through the core's
CsvUtilitywith control characters prefixed. No XLSX. - E-mail report, command
nt_ai:pdf-report(schedulable): totals, the most frequent findings by rule and the publicly linked PDFs with errors, most-linked first (25 at most, the rest counted). Options:--to(required unless--dry-run),--folder="1:/path/"for one report per editorial team,--only-linked,--since-days,--dry-run. Reads stored reports only. - Traffic light in the file list (TYPO3 14, list view): audited PDFs carry a green, yellow or red button with score and counts that links to the findings. The tile view renders no actions and therefore shows none. Users without access to the PDF module see the button without a link.
- Notice in the metadata form of an audited PDF, above the title field: result, score, counts and a link to the findings.
Not included¶
XLSX export, reports addressed by backend group or file mount (use one task
per folder), "new defects since the last report" (the mail counts files with
errors audited within --since-days), dead links, and the traffic light in
the tile view.
[1.25.0]¶
PDF accessibility check, third set from the user review: what the tags of a
"tagged" PDF are worth. No database changes. All PDF reports are rebuilt on the
next nt_ai:pdf-audit run (analyzer version 5); tagged PDFs can get additional
findings.
Added¶
- Quality of the tags (
pdf.structure), for PDFs that have a structure tree: - empty tree — error, and the only finding of this rule (tagging in name only);
- fewer structure elements than pages — warning;
/MarkInfo /Markedmissing or false — warning;/MarkInfo /Suspects— warning;- no heading in a document of three or more pages — warning;
- skipped heading levels (e.g. H1 → H3) — warning; first heading not H1 — notice;
- tables without header cells (TH), with numbers — warning;
- link annotations but no Link tag at all — warning.
The tree is walked from its root in reading order; custom roles are resolved
through
/RoleMap; cyclic references and very large trees are bounded.
Fixed¶
- PHP warning for some fonts.
/DescendantFontsis not always a list; the font check iterated it anyway ("foreach() argument must be of type array"). Found while running the new check over real documents.
Not included¶
The share of page content outside any tag (needs the content streams of every page), list structure, table scope, language changes on elements. The rule is a set of indicators, not a PDF/UA validation.
Checked against hand-built PDFs for every case and against 120 real PDFs from four websites (11 of them tagged): no failures, slowest file 1.3 s.
[1.24.0]¶
PDF accessibility check, second set from the user review: where a PDF is
linked, fillable forms, and a confirmation before document text is sent to an
AI provider. No database changes. All PDF reports are rebuilt on the next
nt_ai:pdf-audit run (analyzer version 4). The usage data needs a current
reference index.
Added¶
- Where a PDF is used. The PDF module shows "public / total" usages per
file — file references and links in text fields, including
t3://file?uid=…links in body text — with a filter "only publicly linked", sorting by usages, two new totals ("publicly linked", "linked and with errors") and, in the findings view, the list of usages with page, record and an edit link. Read from the core's reference index in a handful of queries, independent of the number of PDFs. "Public" means: record and page are neither deleted nor hidden; start/stop times, access groups and hidden parent pages are not evaluated. - Form fields (
pdf.forms): controls of a fillable PDF without a description (/TU, on the widget or a parent field) are an error; form pages without structure tab order (/Tabs /S) a warning; every fillable form gets a notice suggesting an online form. Whether a control is tagged as Form in the structure tree is not checked. - Confirmation before PDF text leaves the server. The alt-text button in
the backend form first asks, naming the provider and the amount (title plus
up to 8,000 characters), and sends only after consent. No question when the
default provider is Ollama (text stays local) or when
pdf.altTextDataPolicyforbids the call anyway. The AJAX routent_ai_alt_textand the bulk command are unchanged; for them the policy setting andaltText.excludePrefixesapply.
Fixed¶
- AJAX route
nt_ai_alt_textfailed for PDFs in 1.23.0 and 1.23.1. The folder-exclusion check added there used a dependency the controller did not have; every PDF request through this route ended in an error (images and the backend form button were not affected). The extension's own interface does not call this route; integrations that do should update.
Documentation¶
- Who sees the PDF module (backend group module permission, file mounts).
Not verified in a browser: the confirmation dialog itself (the endpoint was called without and with consent and answers as intended).
[1.23.1]¶
Same content as 1.23.0, plus the fix from 1.22.3. The tag v1.23.0 was created
from a state that did not contain 1.22.3 (an unconfigured default provider is
named in the error). Use 1.23.1 instead of 1.23.0.
[1.23.0]¶
PDF accessibility check: first set of changes from a user's review against a
stock of about 1,000 public PDFs. No database changes. All PDF reports are
rebuilt on the next nt_ai:pdf-audit run (analyzer version 3) — parsing time
only, no AI calls. Scores of PDFs with errors drop.
Changed¶
- Upload notice shows what the gate reacted to. It listed the first three
findings in rule order and dropped notices only afterwards, ignoring the
threshold: a PDF with the errors "no language" and "not tagged" showed
language, title and fonts — "not tagged" was missing. Findings are now sorted
by severity, only those at or above
pdf.uploadGateSeverityare named (three at most), the rest is counted ("+ 2 weitere Befunde"). pdf.uploadGateSeveritydefaults toerrorinstead ofwarning. With warnings nearly every PDF trips the gate, andblockwould reject usable documents. Installations that have the value stored keep it.- A PDF with at least one error scores at most 49/100. The linear formula rated an untagged PDF without language at 74. Page audits are not affected.
Added¶
- Unusable document titles (
pdf.title, warning): a title that is present but is a file name, a program default ("Microsoft Word - …", "Layout 1", "Dokument-Vorschau"), a scanner job name, digits only or shorter than four characters. The finding quotes the title. - Images in untagged PDFs (
pdf.image.alt, notice): names the number of pages with images. The error stays withpdf.tagged. pdf.uploadMaxSizeMb(default 25): larger PDFs are not audited inside the upload request. The editor gets a notice; the inventory check picks the file up. The existing time limit needspcntland never applied to uploads.pdf.altTextDataPolicy(all/local/off, defaultall): controls whether PDF text may go to an AI provider for an alt-text suggestion.localallows Ollama only,offdisables it for PDFs. Enforced in one place for buttons, upload and bulk runs.
Fixed¶
altText.excludePrefixeswas not applied to the alt-text buttons. Bulk runs and the upload listener skipped excluded folders, the two backend buttons did not. For PDFs they now refuse as well, since document text would leave the server. Images behave as before.
Not included¶
Title equal to the file name (the check does not know the file name), a configurable title list, a visible notice in the interface before PDF text is sent, and the larger topics of the review: documents without a text layer and without raster images, encryption permissions, tagging quality, form fields, veraPDF, where a PDF is linked, export and reports, results in the file list, background processing of the module's batch action.
[1.22.3]¶
Fixed¶
- An unconfigured default provider now says so. The extension ships with
provider.default = anthropic. A site that only stores a key for another provider failed on every call through the publicAiProviderInterfacewith that provider's own "Anthropic API key is not configured." — correct, but it names neither the setting that points there nor the provider that would work, and consumers that swallow provider errors (nt_lingua did) showed nothing at all.ProviderAdapter::complete()now raises aProviderException(1700000031) namingprovider.defaultand the providers usable right now;completeMany()reports the same condition as onenullper prompt, as it does for every failure. There is deliberately no silent switch to a configured provider: which service receives the content is the operator's decision.getName()andgetModel()stay unchecked, since consumers build cache keys and log lines from them.
[1.22.2]¶
Fixed¶
- Read-aloud with the cloud engine raced through the page and went silent
when the endpoint refused a passage (403 text not on the page, 429 rate
limit, 503 budget/busy, 502 provider error). A refused passage was handled
twice: the audio element's
errorevent jumped to the next passage at once, and the rejectedplay()additionally started the browser voice for the previous one. The highlight ran across all passages within seconds, the browser voice lagged one passage behind, and reaching the last passage cancelled it. Measured with every request refused on a page with 30 passages: 30 requests, 20 passages spoken, over after 1.1 s. A single refused passage in an otherwise working run was skipped. Now a failure is handled once per passage: the same passage is read by the browser voice, and only its end moves on. After two refusals in a row the rest of the page is read by the browser voice without further requests (same test: 2 requests, all 30 passages read once, in order). A successful cloud playback resets the counter. - Pause, resume, speed and volume act on the voice that is actually reading. They followed the configured engine, so in cloud mode "Pause" paused the empty audio element while the browser voice kept reading.
- A late answer no longer starts speech after "Stop".
- Browser voice without a usable voice (e.g. no German voice installed):
after three errors in a row read-aloud stops instead of running the highlight
across the page without a sound. An utterance ended by our own
cancel()is no longer counted as an error. - A configuration without browser speech support could alternate between cloud and browser voice without end when the cloud refused; it now stops.
Documentation¶
- Read-aloud: what a refusal means, why passages loaded by JavaScript are always
read by the browser voice, and that
audit.httpAuthUser/audit.httpAuthPassword(since 1.21.0) also apply to the page-text check on sites behind Basic Auth.
Reported against 1.20.0, including the analysis and a patch this fix follows.
The endpoint still answers a refusal as text/plain; the widget does not
distinguish 403 from 429/503.
[1.22.1]¶
Fixed¶
- JavaScript error "n.clearable is not a function" in every form with an AI
text field. The three FormEngine elements (
ntAiText,ntAiAltText,ntAiImageDescription) rendered their<textarea>with the classt3js-clearable. The core only providesclearable()on<input>elements, soform-engine.jsthrew while walking through these fields and stopped: every input after the textarea lost its clear button. Affected were page properties with a meta description, file metadata and image references with an alternative text. Saving was not affected. The class is now only set on the single-line<input>variant ofntAiText, as the core does for its own elements. Present since the elements were introduced; reported against 1.16.1.
[1.22.0]¶
nt_ai can now hand its AI calls to an AI foundation extension instead of talking
to a vendor API itself. Two optional bridges are included: AiM (b13/aim) and
nr_llm (netresearch/nr-llm). The seven built-in providers stay as they are and
remain the default; nothing changes for an installation that has neither
foundation installed. No database changes.
Added¶
- Provider
aim— bridge to AiM (b13/aim). Text and image requests go through AiM'sAiservice with the extension keynt_ai, so they show up in AiM's request log and count against its budgets. API keys and the model are configured in AiM, not in nt_ai. - Provider
nr_llm— bridge to nr_llm (netresearch/nr-llm). Text and image requests go throughLlmServiceManagerInterfaceand use nr_llm's default configuration. - Both appear in the "Standard-KI-Anbieter" setting. A bridge is registered only when its foundation extension is installed, and it reports as available only when the foundation has a usable provider configured. Selecting one without the extension installed leads to the usual "unknown provider" error.
- Both packages are listed under
suggestincomposer.json; neither is a requirement.
Notes¶
- Model and token figures come from the foundation. nt_ai's own model
settings and
%env()%model placeholders do not apply to a bridge. The cost estimate in the token widget only knows the models in nt_ai's price table; a model it does not know is shown with the existing "unknown model" warning. - Bulk runs are sequential through a bridge. The foundations offer no parallel call, so jobs that the built-in providers run concurrently take correspondingly longer.
- One image per request. A request with several images is rejected with an error rather than describing only the first one. nt_ai's own features send one image at a time.
- Image requests through nr_llm bypass its budget and telemetry pipeline.
In nr_llm 0.40.0
vision()does not use the credentials of a provider record created in the backend and fails with "API key identifier is required". The bridge therefore sends image requests to the adapter built from the configured default model (ProviderAdapterRegistryInterface). Text requests go through the regular pipeline. - The nr_llm bridge counts as available when nr_llm resolves a default
configuration;
hasAvailableProvider()alone misses that case. - Both foundations are pre-1.0. Both bridges were run against real
installations: AiM 0.5.0 with
symfony/ai-open-ai-platform0.14 and nr_llm 0.40.0 with nr-vault 1.1.0 — text, image description and alt-text generation each. With AiM the requests appear in its log undernt_ai. A changed foreign API affects one client class per foundation.
[1.21.0]¶
Working with audit results over time: findings can be ignored with a reason and
an expiry, each audit is compared with the previous one, rules can be switched
off per site in the backend. Plus colour suggestions for contrast findings, four
new checks, audits of password-protected sites and stricter handling of AI
output. Adds two tables (tx_ntai_finding_ignore, tx_ntai_rule_setting) and
two settings (audit.httpAuthUser, audit.httpAuthPassword) — run
extension:setup after updating. Scores can change with the next audit because
of the new checks.
Added¶
- Ignore findings with reason and expiry. "Ignorieren" on every finding card ("Alle ignorieren" on grouped ones): reason required, period 7/30/90 days, 1 year or unlimited. Ignored findings leave score and totals at once and stay out of later audits until the period ends; then they return on their own. "Ignorierte Befunde" lists them with reason, user and expiry and restores them. Requires the right to edit the page's content. A finding is recognised by rule, content element and subject — not by its message, which AI rules phrase anew on every run, and not by the hash in a processed image name.
- Comparison with the previous audit. The page detail shows how many findings are new, resolved and unchanged; new ones carry a badge, resolved ones can be listed. Ignored findings do not count as resolved.
- "Regeln" tab (admin). Switch rules off per site in the backend, in
addition to
mod.tx_ntai.audit.disabledRulesin Page TSconfig. - "Wer" column in the recommendations table. Names who usually fixes the problem: Redaktion, Entwicklung or Design.
- Colour suggestion for contrast findings. The finding names the nearest shade of the current text colour that reaches the required ratio, with the resulting ratio — or says that no text colour works on that background.
- New checks:
link.new.window(link opens a new window without saying so, notice),svg.name(SVG withrole="img"without accessible name, warning),landmark.name(severalnav/asidewithout names of their own, notice),table.header.empty(empty header cell other than the top-left corner, warning). - Audits of sites behind HTTP Basic Auth.
audit.httpAuthUserandaudit.httpAuthPassword(both accept%env(NAME)%). The credentials are only sent when fetching the installation's own pages.
Changed¶
- A page that answers 200 with an error output is no longer audited. "Oops, an error occurred!" and similar pages used to produce a report about the error page — and made every real finding of the page look resolved. Such a page is now deferred like an unreachable one.
Security¶
- AI output is reduced to plain single-line text before it is shown for takeover or written. What a model answers is shaped by the page content it was given. Suggestions of the AI rules, generated SEO titles and meta descriptions, and every value saved through "Vorschlag übernehmen", the alt-text dialog or the SEO panel now lose tags, script blocks, control and invisible characters and line breaks; a suggestion that is only a web address is dropped.
[1.20.0]¶
Three additions for sites with many pages: a batch run for SEO titles and meta
descriptions, a background queue with a history of what the AI wrote, and a
prioritised work list above the audit findings. Adds two tables
(tx_ntai_job, tx_ntai_history) — run Analyze Database Structure or
extension:setup after updating. No setting changes, existing data is untouched.
Added¶
nt_ai:generate-seo-meta— SEO titles and meta descriptions in bulk. Finds pages of a tree whose SEO title or meta description is empty, generates them from the page content in the language of the record and saves them through DataHandler, so the edit appears in the record history. Skips shortcuts, folders, hidden pages andno_indexpages; filled fields are only rewritten with--force. Options:--language,--limit(default 25),--title-only,--description-only,--dry-run,--queue. Schedulable.- Background queue (
nt_ai:process-queue).--queuecreates one job per page instead of generating right away; the worker processes a limited number per run. The same job (type, page, language) is never queued twice. A failed job is retried after five minutes, three attempts in total; a job whose worker died is released after 30 minutes. Own job types can be added with a handler taggednt_ai.job_handler. - "Verlauf" tab in the audit module. Lists the values the AI wrote — time, page, field, old and new value, trigger (backend, command line, queue), user and model — and the latest jobs with their state. Editors only see entries of pages and languages they may read; queue totals are admin-only. Recorded are the batch run and accepting a suggestion in the SEO panel.
- "Empfehlungen" table above the findings. One row per distinct problem with priority, where it occurs, what is wrong, how to fix it and why it matters (who is affected, plus the WCAG criterion), ordered by priority and number of occurrences. AI findings, whose texts differ per element, are grouped by rule. The findings list itself is unchanged.
Notes¶
- Finished jobs and history entries are deleted by
nt_ai:process-queueafter--keep-daysdays (default 180; 0 keeps them). - Every generated value costs tokens at the configured provider.
[1.19.0]¶
The nightly PDF check no longer hangs on a single file. Reported after 1.16.1:
re-auditing 4,219 PDFs, the run stuck for good on two bike school-route maps
(3.3 and 6.9 MB, huge numbers of vector paths). Adds one extension setting
(pdf.auditTimeLimit); no schema change, existing reports stay valid (the
analyzer version is unchanged, nothing is re-audited because of this update).
Fixed¶
- PDF check spent minutes extracting the text of pages without images. The text of a page only decides whether an image page is a scan, yet it was extracted for every page. smalot/pdfparser needs time that grows with the square of the content on pages with many marked-content paths — minutes for a vector map. Text is now only extracted for pages with a raster image; the findings are exactly the same as before.
- A "not auditable" file was retried on every run and hung it again. Marks
written by 1.14.1 and earlier count as outdated since the analyzer version
was introduced in 1.16.1, so the command retried them, hung on the same file
and never reached the files behind it — the scheduler task stopped at the
same place every night without an error. Each file is now attempted exactly
once per content (sha1) and analyzer version, failed attempts included: the
command records the file as "not auditable" before parsing it and replaces
the mark with the report once done, so even a killed or hanging run skips the
file next time. A changed file, a newer analyzer or
--forcelead to a new attempt. - "MySQL server has gone away" after a long parse lost the result. A lost database connection is now closed and the write tried once more on a fresh one (MySQL errors 2006 and 2013). Report writes use the QueryBuilder.
Added¶
- Time limit per PDF in
nt_ai:pdf-audit: new extension settingpdf.auditTimeLimit(tab PDF, default 60 seconds,0= off) and option--time-limit Nto override it for one run. A file over the limit is recorded as "not auditable" with the reason. With the PHP extensionpcntl(CLI) the run continues with the next file; withoutpcntl, PHP's own time limit ends the run at that file, which is recorded and skipped from then on. The summary line now also counts files over the time limit, and the command exits with code 1 when there were any, so the scheduler shows the run as failed once. - Memory guard in
nt_ai:pdf-audit: a run that gets close to itsmemory_limit(over 80 % after garbage collection) stops before the next file with a clear message and exit code 1, instead of dying with a fatal error inside an unrelated PDF; the remaining files follow on the next run.
[1.18.0]¶
Security follow-up release. Closes the remaining nt-ai parts of findings N02
(S02) and N03 (S06, parts B and C) of the follow-up review of 2026-10-02. Adds
one extension setting (tts.maxConcurrent).
Security¶
- Accessibility declaration mixed in other sites' data and could be built from partial data (N02). The Lighthouse average of the declaration was taken over all sites, for administrators too, and a non-admin only needed access to the site's root page — reports of subpages he may not read, of languages he may not edit and of PDFs outside his file mounts still went into the figures and the AI prompt. The declaration is a legal statement about a whole site, so it is now either complete or not built at all: every figure (pages, barriers, PDFs and the Lighthouse average) is limited to the requested site and its configured languages, and a non-admin may see the facts, generate and publish only with access to the root page and every page with audit or Lighthouse results, every language with results and every counted PDF of that site. Otherwise the tab shows a localized notice and the AJAX actions answer 403. The site is now authorised before any AI budget is spent. Administrators are not restricted.
- Dashboard widgets ignored language rights (N02). The audit score,
Lighthouse and page score widgets and the score distribution charts
filtered by readable pages but averaged reports of all languages of those
pages. They now also filter by the languages the editor may edit
(
BackendAccess::allowedLanguageUids());LighthouseRepositoryandPageScoreRepositoryaggregates take an optional language list. - Paid backend actions without a budget (N03 B). "Explain finding" and
"Fix alt text" in the audit module, the SEO assistant and the alt text,
image description and text buttons of the record editor called the AI
provider without any rate limit, so switching to them bypassed the limit of
the other actions. All paid backend actions now draw from one per-user
budget (
AiGenerationBudget, settingrateLimitPerMinute), checked after the permission checks and right before the provider call; when it is used up the answer is HTTP 429 and no provider is called.
Added¶
- Concurrency limit for cloud TTS (N03 C). New setting
tts.maxConcurrent(default4,0= off) caps how many cloud generations run at the same time across all visitors. Further new passages get HTTP 503 withRetry-After: 5before any budget is reserved or the provider is asked; the widget falls back to the browser voice as before. Slots are atomic rows intx_ntai_rate_limit(no schema change); a request that dies frees its slot after the request timeout plus 30 seconds.
Changed¶
- Editors without full access to a site's audit data no longer see the declaration facts of that site (they used to see them with root-page access alone).
[1.17.0]¶
Security release. Fixes findings S02, S03, S04 and the nt-ai part of S06 of the security review of 2026-10-02. Several changes alter behaviour for editors and operators — read the upgrade notes.
Security¶
- Audit backend respected page rights but not webmounts (S02). The page
check only tested the "show" permission. A page an editor may show by its
permission bits but that lies outside his webmounts — typically another site
in a multi-site install — was accessible through the AJAX actions and
exports. Worse, the site overview, its totals and rule aggregation, the
Lighthouse tab, the overview report and the dashboard widgets were built
from the reports of all sites; "Alle Websites" and a site chosen in the
URL returned everything. A new
BackendAccessservice now decides for every view, export and AJAX action like the core does: page permission and webmount, plus the language. Lists and totals are built from authorised pages only; the site selector only offers sites the editor can see, and a site in the URL is a filter, never a permission. The PDF module and PDF widget only cover files inside the editor's file mounts, the SEO assistant re-checks the page on every call (its HMAC never expires), and the accessibility declaration needs access to the site's root page. Administrators are not restricted. - Read access was enough to change alt texts (S03). Saving an alt text
from the audit checked the file's read permission and then wrote straight
into
sys_file_metadata. A user with a read-only file mount could change metadata, and table, field, language and workspace rights were bypassed. Reading and editing are now separate checks: saving needseditMeta(a writable file mount), modify rights onsys_file_metadataand thealternativefield, and access to the record's language; otherwise the answer is HTTP 403 and nothing changes. The value is written through DataHandler, so the core's metadata permission check, history and workspaces apply. Only the default-language record is written — before, the update hit every language of the file. The audited page's cache is only cleared for a page the editor may access. - Audit preview token was not bound to its page (S04). The signed token
carried the page uid but was never compared with the requested page; while
valid (120 s) it relaxed visibility for any page. The token now binds page,
language, site and URL path. It is checked against site, language and path
before the page resolver (only then is visibility relaxed), and a new
middleware compares the resolved page with the token after routing and
answers 403 before anything is rendered. Preview renders explicitly disable
the page cache and send
Cache-Control: private, no-store. - Rate limits were not atomic; cloud TTS spoke any text (S06). The rate
limiter read and wrote its counter separately through the cache framework,
so parallel requests read the same count: 8 parallel processes against a
limit of 50 were granted 200 on the dev system. Counters now live in the new
table
tx_ntai_rate_limitand are reserved with one conditional UPDATE (same test: exactly 50). Cloud TTS gets a global daily budget of characters and calls that does not depend on the client IP, reserved before every provider call, and only speaks text that occurs on the requested page of the site (the widget now sends page and language). Cache hits are served before any of these checks and stay free.
Added¶
- Extension settings
tts.dailyCharacterBudget(default 200000),tts.dailyRequestBudget(default 2000) andtts.requirePageText(default on).
Changed¶
tts.rateLimitnow only counts new passages; cached passages are served without touching the limit, as the setting's label always said.- The cache
ntai_ratelimitis no longer registered; its tables (cache_ntai_ratelimit*) can be removed in "Analyze Database Structure".
Upgrade notes¶
- Run
vendor/bin/typo3 extension:setup(new tabletx_ntai_rate_limit) and flush all caches. The frontend set adds a small inline script with the page uid and language for the read-aloud widget; pages cached before the update send no page and their cloud TTS requests are refused (browser voice) until the page cache is flushed. - Editors may see fewer reports, sites, PDFs and smaller totals than before — exactly the part they have access to. Check webmounts and file mounts of editor groups if something expected is missing.
- Saving an alt text from the audit now fails with 403 for editors without
the right to edit file metadata (e.g. read-only file mount, missing
sys_file_metadataoralternativefield rights). - Generating and publishing the accessibility declaration as a non-admin needs access to the site's root page.
- Cloud TTS refuses text that is not on the public page (JavaScript-loaded
content, nt-lingua in-browser translations, simplified language via cookie,
content behind a frontend login, plugin views depending on URL parameters);
the widget reads such passages with the browser voice. Turn
tts.requirePageTextoff to allow free text again — the budgets still apply. Adjust the daily budgets to your expected volume.
[1.16.1]¶
Fixed¶
- PDF audit lost language and title display on untagged PDFs. smalot/pdfparser
answers a missing dictionary key with an
ElementMissingobject, notnull. Its content isfalse, so?->never short-circuits and!== nullis always true.PdfAnalyzerread/StructTreeRootand/MarkInfoin one expression; for every PDF without/MarkInfothe call onElementMissingthrew, the surrounding catch swallowed it, and/Lang,/Outlinesand/DisplayDocTitlewere never read.pdf.langandpdf.titletherefore reported missing language and title display on files that have both. Reported with a PDF 1.3 without tags; thank you. - Tagged PDFs without
/MarkInfocounted as untagged — same cause.pdf.taggedreported them, andpdf.uaIdentifier/pdf.imageAltskipped them. - Outlines were reported for every tagged PDF, because a missing
/Outlinescompared!== null.pdf.bookmarksnever fired for long tagged documents. - Fonts without an embedded font file were never detected, and every font was
counted twice. The font check had the same
!== nullpattern, a font without a descriptor (standard fonts such as Helvetica) ended the count for the page, and smalot lists each font under its name and an index.pdf.fontsnow reports missing font files, including composite (Type0) fonts. - Each catalog fact is now read on its own, so one unreadable entry no longer
costs the others. Indirect values (
/Lang 10 0 R) are resolved.
Changed¶
- Stored PDF reports are rebuilt after the update. Unchanged files are skipped
by their sha1, so a corrected analyzer would never have reached them. Reports
now record the analyzer version (new column
analyzer_version); older ones are audited again on the next scheduler run, without--force. The PDF module marks them as "veraltet" (outdated) until then — previously "geändert", which only fitted a changed file.
Upgrade notes¶
- Run
vendor/bin/typo3 extension:setuponce to add the column. The nextnt_ai:pdf-auditrun re-parses every PDF once — parsing time only, no API calls. For large stocks, spread it with--limitor--folder. - Expect findings to move: false
pdf.lang,pdf.titleandpdf.taggedfindings disappear;pdf.bookmarksandpdf.fontscan appear where they were missed.
[1.16.0]¶
Added¶
- Alt text on upload: choose which arrivals count. New setting
altText.generateOnUploadSource. TYPO3 reports a new file in two ways: metadata is created whenever a file is indexed — an upload, but equally a file TYPO3 merely finds on disk, copied there by SFTP or rsync and picked up by the "update storage index" scheduler task or by an editor opening the folder. Only an upload through TYPO3 raisesAfterFileAddedEvent. Withbackend, the listener reacts to that event inside a backend request only, so copied files, command-line imports and frontend form uploads no longer cost an API call each. The defaultallkeeps the previous behaviour. - Alt text on upload: limit per request or run. New setting
altText.generateOnUploadLimit(0 = no limit) caps how many alt texts one request or scheduler run produces on arrival. Files beyond it are left without an alt text and logged once as a warning; the limit counts API calls, not files that would have been skipped anyway. - Both suggested in a bug report (thank you); the upload alt-text feature is now documented in the configuration reference, where it was missing.
Changed¶
GenerateAltTextOnUploadListenerlistens toAfterFileAddedEventas well. The decision which event counts lives in the newAltTextOnUploadPolicy, so the listener reacts to exactly one of the two per file — no double calls in either mode.
[1.15.1]¶
Fixed¶
- PHP 8.5: alt-text generation aborted on upload.
AbstractHttpProvider::resizeWithGd()calledimagedestroy()three times. The function has had no effect since PHP 8.0 — GD images are objects and are freed when they go out of scope — and is deprecated in 8.5. Where the error handler turns deprecations into exceptions (depending onSYS.exceptionalErrors), the deprecation aborted the generation and no alt text was written. Reported for uploads and for files registered by the indexing scheduler task. The calls are removed. - PHP 8.5: concurrent requests.
postJsonConcurrent()calledcurl_close(), deprecated in 8.5 for the same reason. With deprecations as exceptions, every concurrent batch would have aborted at the first response. Removed as well — not part of the report, found while checking the rest of the code for PHP 8.5 deprecations.
Changed¶
- PHP 8.5 is supported officially.
composer.jsonallows^8.5, and CI runs the unit suite on 8.5 against TYPO3 13.4 and 14. Before, Composer refused to install nt-ai on 8.5 unless the platform check was bypassed. - The requirements in the documentation are corrected to TYPO3 13.4 LTS or 14 LTS
and PHP 8.2–8.5. They still listed TYPO3 12.4 and PHP 8.1, which
composer.jsonhas not allowed for some time.
[1.15.0]¶
Added¶
- Model names can come from the environment. Every model setting —
provider.<name>.modelfor all seven providers, andtts.model— now accepts%env(NAME)%, like the API keys already did. A project can keep model names out of its versionedsettings.phpand move to another model, or away from a deprecated one, by changing an environment variable instead of committing and deploying. Backward compatible: literal model names are returned unchanged. - Unlike an API key, a model placeholder whose variable is empty or missing
resolves to the provider's default model, not to an empty string. A missing
key correctly marks a provider as unavailable; an empty model name would be
sent to the provider and fail every request, so one forgotten ENV entry would
switch off every AI feature. API keys keep resolving to
''as before.
Changed¶
- A model field saved empty in the backend now falls back to the provider default as well. Previously only a missing entry did; an empty one went out to the provider as an empty model name, and every request failed.
[1.14.1]¶
Fixed¶
- The scheduled audit ignored page time control.
AuditTask::findCandidatePages()callsremoveAll()on the query restrictions — legitimately, because the report table'sdeletedfilter has to live in the JOIN condition — and then re-added the page restrictions by hand. The QueryBuilder'sDefaultRestrictionContainercarries four (deleted, hidden, starttime, endtime); only the first two were restored. A page whose end time had passed therefore stayed a candidate, answered 404 like any hidden page, and took one slot in every run. Unlike a page under maintenance it never becomes auditable again on its own, so it returned week after week. Reported by an installation that recognised its own expired registration page in the new deferral table. Only the Scheduler task was affected:AuditCommandandAnalyzePagesCommanddo not callremoveAll()and get the time restrictions from the default container. - A page with a future start time is excluded for the same reason and joins the rotation by itself the moment it goes live. Auditing a page deliberately before publication remains possible through the signed preview token — that is a manual, explicit act rather than the business of the nightly rotation.
[1.14.0]¶
Fixed¶
- Translated page records were audited under the wrong URL and could never
succeed. The candidate queries filtered by doktype, deleted and hidden, but
not by
sys_language_uid, so translation rows were picked as targets in their own right. A page is audited by building the target language's URL for the default-language UID; taking a translation row instead produces the translated slug without the language prefix —/about-us/rather than/en/about-us/— a URL that does not exist. Every such page answered 404. On a bilingual site this was roughly half of all candidates: on the instance where it was found, 52 of 53 deferral entries after a run were translation rows. Before 1.13.0, when the pause did not yet survive a run, they could fill the batch entirely and stop the rotation. The same gap existed in four places and is closed in all of them:AuditTask::findCandidatePages(),AuditCommand::findCandidatePages(), and both candidate paths ofAnalyzePagesCommand(findAllNormalPages()andcollectChildren()).LighthouseCommandalready did this correctly — its comment states the reasoning — and served as the model.
Upgrade notes¶
- Nothing to configure, but check your language coverage. Translated records leave the rotation by themselves after the update; leftover deferral entries from earlier runs expire on their own. If you relied on those rows to have any audit of your translated pages, note that they never produced a valid result — they only ever recorded a 404. To audit a second language, set up one task per language, each with its own Language setting. They share the deferral table.
- A page with no translation in the target language answers 404 under a
strictfallback and is set aside for a week as a result, which is correct and harmless; with a fallback configured it is audited as rendered.
[1.13.1]¶
Changed¶
- A deferral row now says where its pause came from. The
reasoncolumn carries the origin —Retry-After: 1800s,no Retry-After, default,clean 404/410, or the position in the failure escalation. Reported from an installation verifying 1.13.0: their maintenance page sendsRetry-After: 3600and the built-in fallback is also one hour, so the measured timestamp was identical either way. They could only prove the header was read at all by temporarily setting it to 1800 — one experiment too many for a question the stored row should answer itself.
Documentation¶
- Installation: how to replace the extension directory when deploying by rsync or
tar instead of
composer installon the target. Removing the old directory before copying the new one leaves a window in which the extension is present but incomplete; a request landing in it reports a class-not-found error for a class that does exist. Diagnosed in a production installation and contributed by the site that hit it — it applies to any TYPO3 extension, not only this one.
[1.13.0]¶
Fixed¶
- The audit's cooldown for unreachable pages never took effect.
AuditTaskkept the cooldown in two properties on the task object, and their docblocks stated it was "persisted with the task between runs" — it was not, and could not be. The scheduler saves a task before it executes (Scheduler::executeTask()), andSchedulerTaskRepository::updateExecution()narrows the written fields to nextexecution/disable/execution_details; nothing writes a task's parameters back afterwards. Every cooldown was therefore forgotten the moment the run ended. The consequence was severe: an unreachable page writes no report, so itslast_checkedstays 0 andORDER BY last_checked ASCpicks it first again on the very next run. Enough such pages fill every batch and the rotation stops entirely — reported from an installation with 27 sites where 34 unreachable pages at a limit of 25 reduced the output from 144 audit reports per hour to one or two, for 16 hours, while the task reported red on every run. The state now lives in the new tabletx_ntai_audit_deferral, one row per page and language, which survives a run and is shared by all audit tasks instead of each keeping its own copy. - HTTP 503 and 429 were counted as technical failures. A site in maintenance mode answers 503 on purpose, and a rate limiter answers 429 on purpose; both say "come back later". They are now treated like a clean 404/410 — a result, not a failure — so a batch made up of them no longer trips the "every page failed" rule and turns the task red. A site under maintenance now steps out of the rotation by itself and returns when it is back, with no configuration.
Retry-Afteris honoured when the server sends one, so the pause matches what the server actually asked for instead of a guess. Both header forms are accepted (a delay in seconds and an HTTP date); a value in the past, a malformed value or an absurd one cannot park a page (capped at a week).
Added¶
- Table
tx_ntai_audit_deferralrecords why a page was set aside and until when, so "why is this page not being audited?" is answerable:SELECT page_uid, status_code, FROM_UNIXTIME(deferred_until), reason FROM tx_ntai_audit_deferral;Runextension:setuponce after updating so the table is created. - Documentation section "Pages the audit cannot reach" in
Documentation/Configuration/Index.rst(the tree that ships with the package).
Note¶
- A per-task exclusion list for sites in maintenance was considered and deliberately not built: with 503 handled correctly, such a site removes itself from the rotation for exactly as long as it says it needs, which is better than a second list that has to be maintained by hand and removed again afterwards.
[1.12.3]¶
Documentation¶
- The Scheduler-arguments section now ships with the extension. 1.12.2 added
it to
docs/(MkDocs) only — anddocs/carriesexport-ignore, so it never reaches an installed instance. Anyone searchingvendor/netthinks/nt-ai/during a fault diagnosis, which is the first place to look, did not find it. It is now inDocumentation/Configuration/Index.rst, which is part of the distributed package. Reported from outside, after the section could only be located viagit diffagainst the public repository. - The section also states the consequence that surprised that reporter: a run in
which nothing could be measured exits non-zero, so
nt_ai:lighthouseon a local DDEV instance always ends with exit code 1 — correct, because PSI cannot reach a local URL, but it makes the command unsuitable as a local smoke test. (This is not new behaviour; the rule predates 1.12.2.) Documentation/ChangeLog/Index.rststopped at 1.2.0 while the extension was at 1.12.x, which reads as an abandoned project. It now points to the shippedCHANGELOG.mdas the authoritative source and marks its own entries as historical.- Fixed a typo in the Lighthouse section ("keepin" → "keeping").
[1.12.2]¶
Fixed¶
- An empty optional
pageIdfrom a Scheduler task no longer becomes page 0. A task created in the backend stores every argument it was configured with, so an unsetpageIdarrives as""rather thannull. Bothnt_ai:analyze-pagesandnt_ai:lighthousecast that straight toint, and(int)'' === 0: instead of measuring every page, they measured exactly one page that does not exist. The same case was already handled for thestrategyoption; the argument now gets the same treatment, and apageIdof0is rejected with a clear error rather than one unreachable-page warning per language. nt_ai:analyze-pagesno longer reports success for a run that scored nothing. A page that cannot be reached counts as a result, not an error — by design, so that record folders and clean 404s do not turn a healthy nightly run red. But when a run scored nothing, had nothing already up to date, and every single candidate was unreachable, that is not a quiet day: it is a task pointed at a page id that cannot work. Combined with the bug above, such a task could run green for weeks without ever writing a score. That case now warns, explains the likely cause, and exits non-zero. A mixed run is unchanged: as long as anything was scored or was already up to date, unreachable pages alone never turn the task red.
Documentation¶
- Configuration: a section on optional arguments in Scheduler tasks — why an
empty
pageIdis stored at all, that it now means "all pages", and that tasks created from a script should passarguments: [].
[1.12.1]¶
Fixed¶
- The page module panel and the audit module no longer disagree about a translated page. The panel shows the report for the language currently being edited, but its "Open in module" link passed only the page id. The module fell back to the default language and therefore presented a different report — different score, different error counts — for what looked like the same page. A translation is audited under its own URL, so the two reports legitimately differ; the link now carries the language so both views stay on the same one. The same applies to the Lighthouse link.
[1.12.0]¶
Added¶
AiService::complete()— run your own prompts through the front door. Every other method onAiServicebuilds its system prompt throughPromptBuilder, which is right for the tasks nt_ai defines and leaves no way in for an extension that has a prompt of its own and has measured what that wording does. The only route left was to reach pastAiServiceto the provider locator — and everything that does so skipsTokenUsageService, so the calls never appear in the dashboard while the money is being spent.complete($systemPrompt, $userPrompt, $providerId, $context, $task)takes both prompts verbatim, pins the call to a named provider, and records the usage like any other call.$contextand$taskare free labels and show up as the corresponding columns.
Found in t3radar, which had exactly this problem: a deterministic provider chain built on the locator from 1.4.0, and a token dashboard that stayed empty.
[1.11.0]¶
Added¶
- Per-site filter in the accessibility audit backend module. On a multi-site installation (e.g. many school websites in one instance) the Site overview and the Lighthouse tab listed every page of every site — a global list that is useless for the editors of a single site. A "Website" dropdown next to the tabs now scopes both tabs (page table, "most frequent issues" aggregation, totals and the Lighthouse table) to the pages of the chosen site; "Alle Websites" keeps the previous cross-site view. The choice is remembered per backend user and carried across tabs, and defaults on first use to the site of the page currently selected in the tree. The dropdown only appears when more than one site is configured, so single-site installations are unchanged. The page-detail and declaration tabs are inherently single-page and are not affected.
Changed¶
WidgetSiteScopegained a publicoptions()method (site identifier => label list) shared by the dashboard widgets and the new module filter; it is now registered as a service so the audit module can reuse it.
Fixed¶
link.distinguishableno longer fires a global, unsuppressable error on every page. The rule scans the stylesheet text fora { text-decoration: none }— the audit is HTTP + static DOM and cannot measure the rendered link, so a single theme-wide declaration flagged an Error (−10 points) on every page even when content links were perfectly distinguishable, burying actionable findings. Now: the finding is suppressed when the same stylesheet re-adds a non-colour indicator in a content-scoped selector (e.g..ce-bodytext a { text-decoration: underline }— exactly the correct fix); it is a Notice (not an Error) reported once per page and worded as a theme-wide CSS hint, not a per-link defect; and the snippet now shows the offendingselector { … }declaration instead of the first 300 characters of the (minified) stylesheet. The per-anchor inlinetext-decoration:nonecheck is unchanged in intent (now a Warning).- Individual audit rules can finally be disabled per page tree via TSconfig. The
disabledRulesmechanism existed but no caller populated it, so the documented TSconfig switch did nothing.AuditService::auditPage()now readsmod.tx_ntai.audit.disabledRules(comma-separated rule IDs) and merges it with any programmatic list — effective for the command, scheduler task, backend module and on-save inline panel. Example:mod.tx_ntai.audit.disabledRules = link.distinguishable.
Changed¶
link.distinguishableis now listed in the built-in rules table, and the docs state its limitation plainly: it is a CSS-text heuristic, not a rendered-link measurement. A true per-link check (link colour vs. surrounding text,:hover/:focusper G183) needs a headless browser and is tracked as a separate feature.
[1.10.1]¶
Fixed¶
nt_ai:analyze-pagesno longer reports the scheduled task red when a page is merely a result rather than a failure. The command exited with code 1 as soon as one page was counted an "error", which turned the scheduler task red on every run — for two cases that are not technical failures and that the audit task already treats as results since 1.8.6: a page without a site (the central record folders for forms, footer and navigation content aredoktype = 1but reachable through no site) and a clean HTTP 404/410 (a news/events detail URL with no record). Pages without a site are now skipped up front via theSiteFinder, and a clean 404/410 is counted as "unreachable", not "error". The exit code is 1 only on genuine failures (timeout, provider error, write error), so a healthy instance stays green. Neither case consumes a--limitslot any more, so a small--limitreaches real pages instead of being used up by these two page types (0 analyzed). The run summary now readsN analyzed, N unchanged, N unreachable, N error(s).
Changed¶
- The content-change detection is fronted by a small
PageScoreServiceInterface, so the analyze-pages command depends on the score-service behaviour rather than the final class. No functional change; it keeps the command unit-testable (the exit-code contract above is now covered by unit tests).
[1.10.0]¶
Added¶
nt_ai:lighthouse --changed-onlymeasures only pages whose content changed since their last measurement. A full Lighthouse run measures every page on every invocation, so a scheduled run's wall-clock grows with the page tree. With the flag a page/language/strategy is skipped when its latesttx_ntai_lighthouse_reportentry is newer than the last change to the page or its content elements — the sameMAX(pages.tstamp, MAX(tt_content.tstamp))comparison asanalyze-pages, but per strategy (abothrun never skips mobile just because desktop is recent). No schema change. Note: Lighthouse scores also drift without content edits (server, assets, third parties), so a periodic full pass is still worthwhile.nt_ai:lighthouse --limit N(-l) caps the number of pages measured per run (0 = no limit); skipped, unchanged pages do not count.
Changed¶
- The content-change detection behind
--changed-onlyis now a sharedPageChangeDetectorservice used by bothnt_ai:analyze-pagesandnt_ai:lighthouse, instead of being duplicated in the commands. No behavioural change toanalyze-pages.
[1.9.0]¶
Added¶
nt_ai:analyze-pages --changed-onlyre-scores only pages whose content actually changed. Every run previously re-analysed every page and re-sent its text to the AI provider, so a monthly schedule repeated the full cost even though only a fraction of pages change (and it was the one command that sends page text externally, which is unwelcome whenaudit.aiRules = 0). With the flag a page/language is skipped when its latesttx_ntai_page_scoreentry is newer than the last change to the page or its content elements — the comparison usespages.tstampandMAX(tt_content.tstamp), so an edit to a single content element is not missed. No schema change. A scheduled daily or weekly run now costs nothing on quiet days and picks up changes promptly instead of waiting for the next full inventory run.nt_ai:analyze-pages --limit N(-l) caps the number of pages analysed per run (0 = no limit). Skipped, unchanged pages do not count against the cap, so a catch-up run after a large editorial change processes at most N changed pages instead of everything in one pass — mirroring the per-run cap of the audit task.
[1.8.6]¶
Fixed¶
- Pages that permanently return HTTP 404 no longer block the scheduled audit or
mark it as failed. A page answering a clean 404/410 (e.g. a news/events detail
URL with no record) was counted as a technical failure: it wrote no report, kept
last_checked = 0, was re-selected first on every run, and a batch made up entirely of such pages tripped the "every page failed" rule and turned the task red on a healthy instance. A 404/410 is now treated as a result — excluded from the "every page failed" rule and deferred for a week — while genuine technical failures get an escalating cooldown (6 h → 24 h → 3 d → 7 d) instead of the previous flat six hours. Pages removed from the search index (no_index, EXT:seo) are skipped entirely. nt_ai:pdf-auditno longer dies silently on one file and blocks the rest. A memory-heavy PDF exhausted the CLImemory_limitinsidesmalot/pdfparser, a fatal that try/catch cannot catch: the run died with exit 255 and no output, and because the file never got a report it stayed first in the queue forever. A shutdown handler now records the in-progress file as "not auditable" (score 0) and writes a note to stderr, so the next run skips it and the crash is visible; files that throw are marked the same way. A new--max-size-mb Noption skips PDFs larger than N MB (also recorded) before parsing, for hosts with a tightmemory_limit. The memory requirement is now documented.
[1.8.5]¶
Added¶
- The PDF-accessibility dashboard widget can be scoped to a single folder. In a multi-site install most PDFs are never linked from a page, so the site scope of the other widgets does not fit them; files are organised by folder instead. The widget now has an "Ordner" setting (default "Alle Ordner") that filters the figures to a top-level folder of the file mount, and — like the site-scoped widgets — shows the selected folder as a badge (also in the empty state). Filtering uses the file's live storage path, so it stays correct even after a document is moved.
nt_ai:pdf-audit --folder="1:/GaD/"restricts a run to one folder, so the check can be split into per-folder scheduler tasks staggered over time instead of auditing every PDF in a single run.
[1.8.4]¶
Changed¶
- The site-scoped dashboard widgets now show which site they are scoped to. The Audit-Score, Page-Score and Lighthouse widgets carry a small pill naming the selected website ("Alle Websites" by default), so their figures are never read out of context — shown in the empty state too, before any data exists. The scope is still changed via the widget's settings (gear icon); this only makes the current choice visible.
[1.8.3]¶
Fixed¶
- The scheduled audit no longer stalls on pages that belong to no site. A page
that fails to audit — most commonly a shared record container (form, footer or
menu content) that lives outside every site — wrote no report, so its
last_checkedstayed 0 and it was re-selected first on every run. Once enough accumulated they occupied every slot and no new reports were produced at all, while the task still reported success.AuditTasknow (a) skips pages that belong to no site when choosing candidates, and (b) records each failed attempt and defers that page for six hours, so any failure cause (missing site, timeout, redirect) rotates out of the queue instead of blocking it. The deferral map is persisted with the task between runs.
Added¶
- Dashboard audit widgets can be scoped to a single site. In a multi-site
installation an average across every site tells a single site's editors nothing.
The "Audit-Score", "Seiten-Score" and "Lighthouse" widgets gained a "Website"
setting (default: all sites) that restricts their figures to the pages of one
site, resolved via
SiteFinder. (The token widgets stay instance-wide by nature; the PDF widget is file- rather than page-scoped.)
Changed¶
- The token-usage widget explains the "enabled but empty" state. When tracking
is on but nothing has been recorded this month — the common state right after
setup — the widget now says so instead of rendering a blank view that is
indistinguishable from a fault. The disabled message additionally names the exact
setting key (
tokenTracking.enabled).
[1.8.2]¶
Fixed¶
- Alt-text length capping no longer produces sentence fragments that look complete. When a generated alt text exceeded the character limit, the fallback cut at a word boundary (or, failing that, mid-word) and appended a full stop — so a truncated fragment read as a finished sentence, misleading both editors and the read-aloud feature ("… vor dem Haupteingang der Schule und hält."). The cap now (a) uses the last complete sentence that fits, with no minimum-length gate, so a short leading sentence is kept instead of discarded; (b) never breaks mid-word; and (c) marks a genuinely truncated text with an ellipsis, never a period. Before truncating at all, an overrun now triggers one retry with a stricter length instruction, and that length limit is stated in the output language (a German limit is followed far more reliably than the previous English-only line) — so German, compound-heavy descriptions come back as a whole sentence within the limit rather than being cut.
- The audit names the original file, not TYPO3's processed preview. Image
findings showed the rendered
csm_…_<hash>.jpgname, which an editor cannot find in the file list. Findings now display the original file name (resolved viasys_file_processedfile, with a decoration-stripping fallback). - Generating an alt text or description for a file outside the editor's file mount now returns a clear message instead of the raw "Access to table sys_file_metadata … was denied by a ModifyRecordEditUserAccessEvent listener" exception: „Diese Datei liegt außerhalb Ihrer Ordner. Bitte wenden Sie sich an die Administration."
[1.8.1]¶
Fixed¶
- The audit tabs no longer drop the selected page, so the declaration can be
generated again. The "Site-Übersicht", "Lighthouse" and "Erklärung" tab
links were built without the current page id, while the module reads the site
to work on from exactly that
id. Switching tabs therefore lost the tree selection: "Erklärung" showed "Bitte zuerst eine Seite … auswählen" and its generate button stayed hidden — even for administrators — and "Seitendetail" fell back to "(Seite im Baum wählen)". The three cross-tab links now carry the current page id, so the selection survives a tab switch and the declaration resolves its site. - The "Erklärung" tab is hidden from users who may not use it. 1.7.0 secured
the generate/publish actions and hid the workbench, but the tab itself stayed
visible and clickable, so an editor could land on a page where nothing could be
done. The tab is now omitted unless the user is an administrator or
declaration.editorsMayPublishis enabled — the same permission check that already guards the actions. Operators who hid the tab with custom CSS can drop that workaround.
[1.8.0]¶
Fixed¶
- The declaration's PDF statistics are now scoped to the site. After 1.7.0 made the page statistics site-specific, the PDF document counts (and the synthetic "Dokumente (PDF)" barrier) still aggregated every document in the installation, so one site's declaration reported documents belonging to other sites. Documents are now attributed to a site by reference — a PDF counts when a non-deleted file reference to it lives on a page or record of that site ("documents published on this site"); a PDF linked from no site is claimed by no declaration instead of installation-wide. Applies only when a site context is known; single-site installs are unchanged.
Added¶
ntai.declaration.pdfPathPrefixsite setting (and the globaldeclaration.pdfPathPrefixfallback) to scope PDF documents by folder instead of by reference — e.g.1:/documents/schule-a/. When set it is authoritative (deterministic and auditable, fits a per-site folder layout and also captures PDFs linked only from RTE text); when empty, reference-based attribution applies. Added to thenetthinks/nt-ai-declarationset.
[1.7.0]¶
Makes the accessibility declaration site-aware and safe for multi-site installations (reported on an install with 26 school sites sharing one page tree), and stops it from asserting features or measures that are not actually in place.
Security¶
- Generating and publishing the declaration is now administrator-only by
default. The declaration is a legally binding statement of the operator, but
the two write actions (
generateDeclarationAction,publishDeclarationAction) had no permission check beyond module access — any editor group that could see the audit module could publish one. TYPO3 has no per-module-tab permission, so operators previously had to choose between giving editors the whole module or none of it. Both actions are now gated onisAdmin(); the newdeclaration.editorsMayPublishswitch (default off) re-opens them to editors where wanted. Editors still see the tab and the facts.
Fixed¶
- The declaration target page is now resolved within the calling site.
DeclarationPublisher::resolveTargetPageUid()searched the whole page tree for a/barrierefreiheitslug and returned the lowest UID — in a multi-site install that is an arbitrary (possibly orphaned) page belonging to no site or to the wrong school. The page is now taken from the site of the page selected in the module tree: its per-sitentai.declaration.targetPageUidsetting first, then a/barrierefreiheitpage that actually belongs to that site. The globaldeclaration.targetPageUidis no longer used when a site is known (it is a single value shared by all sites and would collide across them). - Organisation, feedback contact and enforcement body are now per site.
DeclarationDataServiceread all four fields from the global extension configuration, so a feedback link for one school pointed at another. They are now read from the site'sntai.declaration.*settings with the global value as fallback, and the page statistics (total/clean/warning/error pages, known barriers) are scoped to the calling site instead of aggregating the whole installation. Per BITV 2.0 / Directive (EU) 2016/2102 the feedback mechanism must lead to the responsible body. - The "regular automated audit" measure is only claimed when it is true.
The measure was appended unconditionally; the declaration asserted a recurring
automated check even where the
AuditTaskscheduler task is switched off (e.g. during a migration). It is now added only when a non-disabledAuditTaskexists in the scheduler, mirroring the existing nt-lingua check.
Added¶
- New site set
netthinks/nt-ai-declarationcarrying the per-site declaration settings (ntai.declaration.targetPageUid,.organisation,.feedbackContact,.enforcementBody,.enforcementUrl). It ships no frontend TypoScript, so a site can manage its declaration data without enabling the frontend widget. Include it per site to edit these in the Sites module; the values also work when set directly inconfig.yaml. declaration.editorsMayPublishextension setting (see Security).
Changed¶
- The assistant, read-aloud and "simple language" measures are no longer
pre-filled.
declaration.measuresnow defaults to empty; the accessibility assistant and the read-aloud (TTS) measure are instead auto-derived from whether thenetthinks/nt-aifrontend set is actually active for the site (via itsntai.a11yWidget.enabled/ntai.tts.enabledsettings). Previously the factory default claimed a widget, a read-aloud feature, "sufficient colour contrast" and "alt texts" even on sites that shipped none of them. On installs created before this change the old string is already stored insettings.phpand should be cleared once.
[1.6.0]¶
Fixed¶
- The scheduler audit now rotates across the entire page tree.
AuditTasksorted the candidate pages byuidascending, without taking into account when a page was last checked — every run checked the same pages with the lowestuid, and the rest of the tree was never reached (reported with 1,216 pages andlimit = 25: the same 25 pages every night, 1,191 never checked). The candidates are now sorted by their last report date — never-checked pages first — via a LEFT JOIN ontx_ntai_audit_report. A fixedlimitthus covers the complete inventory across multiple runs. The report table'sdeletedfilter is placed in the JOIN condition so that never-checked pages (NULL) are not dropped; the page restrictions (deleted/hidden) are set explicitly. audit.aiRulesnow actually disables the AI audit rules. The setting had a getter (ConfigurationService::areAiRulesEnabled()) that was never evaluated anywhere — the five AI rules ran as soon as any provider key was configured, even withaudit.aiRules = 0.AuditServicenow checks the toggle as well, so operators can keep page text away from the AI provider while the alt-text feature remains usable at the push of a button. Important for the data protection documentation ("which data goes when where").- The custom FormEngine elements evaluate the base render hooks.
ntAiAltText,ntAiImageDescriptionandntAiTextnow callrenderFieldInformation(),renderFieldControl()andrenderFieldWizard()— in addition to the field's TCAdescription(which is already rendered viarenderLabel()). Adescriptionset on the field — e.g. a data protection notice right at the AI button — is now displayed instead of being silently discarded.
Documentation¶
- Installation: The backend modules "AI Tools" and "Accessibility Audit" are
registered with
access = userand are therefore invisible to editor groups until they are granted via be_groups → "Module" (groupMods) — added to the installation guide.
[1.5.0]¶
Added¶
- Self-hosted read-aloud (text-to-speech). A ReadSpeaker-style read-aloud feature, admin-switchable between two engines: the browser's Web Speech API (local, free) or cloud TTS (premium voices, generated server-side and cached — the API key stays on the server). It reads
#maincontentblock by block with highlighting, a player bar and keyboard control, and is clearly labelled as an addition to — not a replacement for — screen readers. Placeable inside the accessibility widget and freely in templates ([data-nt-tts]/ theReadAloudFluid partial). Ships astts.js,TtsServiceandTtsMiddleware; configured via the site settings (engine / rate / scope) and the nt-ai extension configuration (cloud provider / key / voice). - Alt text and image description directly on the image in the content element. The
AI buttons previously only appeared on
sys_file_metadata(the file list). An image in a content element is asys_file_referencewith its ownalternative/descriptionfields — these are now wired up as well, so that the text can be generated where the image sits, without the detour via the media module. A newFormAiFileResolverresolves both contexts in one path (metadata viafile, reference viauid_local), and the request's HMAC additionally binds the table.
Changed¶
- Image description in the language of the record. The image-description controller previously always generated in the default language; now a description on an English page comes back in English — as the alt text already did.
[1.4.0]¶
Changed¶
searchFieldsremoved from the TCA oftx_ntai_audit_report. TYPO3 14 no longer evaluates the entry in thectrlsection and reports a deprecation at bootstrap; which fields are searchable is now declared assearchableon the respective column. As with the point below, this affects every test suite that runs withfailOnDeprecation.ext_emconf.phpremoved, metadata moved intocomposer.json. TYPO3 14.3 discardsext_emconf.phpand expects insteadextra."typo3/cms".versionandextra."typo3/cms".Package.providesPackages. Without both, the PackageManager reports a deprecation as soon as the extension is installed in a TYPO3 14.3 project — which turns every test suite withfailOnDeprecationred. The version is deliberately placed in theextrablock and not at the top level, because the package is delivered via Git tags.
Added¶
- Public API for addressing a specific provider.
Netthinks\NtAi\Provider\AiProviderLocatorInterface(@api,public: true) returns, viaget('openai'), a provider that stays permanently bound to this identifier. Previously, the public interface always resolved the configured default provider — correct for the normal case, but it made it impossible to build a fallback chain: an external extension that wants to try OpenAI first and Anthropic on failure could not name the second provider and would have had to access the internals (ProviderRegistry). In addition,getAvailableProviders()returns the registered and configured providers, so that a chain can skip unconfigured providers. The returned objects implement the sameAiProviderInterface— calling code remains unchanged.
Changed¶
ProviderAdapterno longer maps requests and responses itself, but delegates to the locator. Behavior unchanged: the default provider is still resolved anew on every call, so that configuration changes take effect without rebuilding the container.
Fixed¶
- Developer documentation named the wrong interface for custom providers.
docs/developer.de.mdanddocs/developer.en.mdreferred to the publicNetthinks\NtAi\Provider\AiProviderInterface, but showed the methods of the internalNetthinks\NtAi\Service\Provider\AiProviderInterfaceandAiResultinstead ofAiResponseas the return type. Anyone who followed the example got a class that could not be tagged as a provider. The RST version was already correct.
[1.3.6]¶
Fixed¶
- Accessibility window protruded at the top on flat screens. The
height of the panel was bound to the window height with
min(80vh, 640px), not to the space actually available above the button. On an iPad in landscape orientation (695 px visible height), the heading and close button were outside the image. The height now calculates with the configured edge margin, the button size and a little breathing room;dvhadditionally accounts for the retracting and extending bars of mobile browsers. Measured at 1024x695, 1024x600, 768x1024 and 390x664: the panel stays completely within the image.
[1.3.5]¶
Fixed¶
- Score-alert emails failed under TYPO3 v14:
AlertServicesent viaMailMessage::send(). This method was removed in TYPO3 v14 —MailMessageinherits from Symfony'sEmail, which does not havesend(). As soon as a threshold was actually breached, the send would have aborted with a fatal error. The send now runs viaMailerInterface; the system sender is automatically added as before.
[1.3.4]¶
Changed¶
- Deliver the a11y/TTS init script CSP-nonce-capable: The inline script that
sets the
data-nt-*configuration early on the<html>tag (including dark mode before the first paint) is no longer output as a raw<script>viapage.headerData, but viapage.jsInline(PageRenderer). As a result, with an active Content Security Policy, it automatically receives the request nonce (script-src 'nonce-…') and is no longer blocked. It still runs synchronously in the<head>(no dark-mode flash). A prerequisite for switching to an enforcing frontend CSP in the sitepackage.
[1.3.3]¶
Fixed¶
- "Erklärung" tab also visible in the page detail: The tab previously appeared only in the site overview and in Lighthouse, not in the page detail view. It is now displayed (like the Lighthouse tab) in all audit views.
[1.3.2]¶
Changed¶
- Accessibility declaration without internal tool names: The draft spoke of "automated audit by nt-ai" — meaningless to readers. It is now phrased neutrally ("self-assessment using automated accessibility testing tools as well as supplementary manual review"); the measures texts also no longer name any product names (nt-ai/nt-lingua). The prompt additionally instructs the AI not to name any software products.
[1.3.1]¶
Fixed¶
- Lighthouse scheduler still reported an error: The command returned
FAILUREas soon as ONE page failed — but without a PSI API key, a run across all pages inevitably hits Google's rate limit (429), so that individual pages fail and the scheduler marked the whole task as failed. A monitoring run is now only considered a failure in the case of a total failure (not a single page successful); partial failures are warnings. Additionally, a hint aboutlighthouse.apiKeywhen errors occur without a key. - Accessibility declaration: The notice box in the module ran across multiple
columns because the flex container arranged each inline element (icon,
<strong>) as its own column. The text now sits in its own block — the notice again flows normally on a single line around the icon.
[1.3.0]¶
Added¶
- AI alt texts for PDF documents: PDFs can now also receive an alt text
(relevant when a PDF is embedded as a preview image with a link to the document).
Since a PDF does not go to the vision AI, its text content is
extracted (
PdfTextExtractor) and summarized by the text AI into a concise alt text. - Works at the same "Alt-Text generieren" button in the file metadata form (detects PDF automatically) as well as at the generic AJAX endpoint.
- New CLI switch
nt_ai:generate-alt-texts --include-pdf(opt-in; without it, it stays with images, so that existing cron runs remain unchanged). - Scanned PDFs without a text layer are skipped with a clear message.
Changed¶
- Extension configuration made clearer: The seven individual provider
tabs (Anthropic, OpenAI, Gemini, DeepSeek, Mistral, Groq, Ollama) are combined into
one "KI-Anbieter" tab (labels with provider prefix),
Rate-Limitunder "Allgemein", the "Barrierefreiheitserklärung" tab shortened to "Erklärung" — 16 tabs → 9, so that the tab bar fits into the window. Only categories/labels changed, no keys — existing values remain.
Fixed¶
- Accessibility declaration: "Leichte Sprache" (easy-to-read language) is no longer listed across the board as a measure. The simplification language is now configurable (default: only "Einfache Sprache"), so that "Leichte Sprache" only appears when actually enabled. Multilingualism (nt-lingua) is still added automatically.
[1.2.0]¶
Added¶
- Accessibility declaration generator (new "Erklärung" tab in the audit module): generates, from the existing audit data, the draft of a declaration on accessibility (based on the BITV template text / BFSG § 14, EU 2016/2102) and publishes it after editorial approval on the accessibility page.
DeclarationDataService: aggregates the conformity status, affected WCAG barriers (identical grouping as the BFSG overall report), PDF results, measures (config + automatically detected nt-lingua/nt-ai audit) and the audit date.DeclarationGenerator: AI draft de/en, only from the supplied facts, missing mandatory information as "[bitte ergänzen]".DeclarationPublisher: publication via DataHandler (versioned, access-checked). On the first time, the existing declaration element is adopted and updated (no duplicate); the managed record is remembered per page/language insys_registry.- New extension settings tab "Barrierefreiheitserklärung" (
declaration.*): target page, organization, feedback contact, enforcement body, measures. - AJAX routes
nt_ai_declaration_generate/nt_ai_declaration_publish.
[1.0.34]¶
Fixed¶
- Lighthouse scheduler task failed ("Task failed to execute successfully",
task type
nt_ai:lighthouse): The scheduler passes all options — an unset strategy arrived as an empty string and went unchecked into the PSI request (strategy=""). Empty/invalid values now fall back to the configured default (lighthouse.strategy).
[1.0.33]¶
Added¶
- PDF accessibility phase 3a: Dashboard widget for PDF results and a dedicated "Dokumente" section in the BFSG overall report (BFSG also covers documents).
[1.0.32]¶
Changed¶
- Docs: PDF accessibility fully documented (user manual + configuration).
[1.0.31]¶
Fixed¶
- Upload gate warning disappeared after 5 seconds. TYPO3 hides notifications
without an explicit client duration for all severities except
errorafter 5 s — anderrorseverity is not an option, because it flips the upload response tohasErrors=trueand the stored file would be incorrectly marked as "failed". A newpdf-upload-notice.jsis loaded once into the backend main frame viaAfterBackendPageRenderEvent(all notifications render there, no matter which module iframe they come from) and sets the duration to 0 for the gate messages — they stay put until the editor actively closes them. Only active when the gate is switched on.
[1.0.30]¶
Changed¶
- Upload gate: default severity is now
warning(pdf.uploadGateSeverity) — the gate thereby also takes effect by default on warnings (e.g. missing document title), not only on errors.
[1.0.29]¶
Added¶
PDF accessibility phase 2: upload gate
- New
PdfUploadGateListener(PSR-14): checks PDFs at backend upload —pdf.uploadGate=off(default) /warn(file is saved, FlashMessage with findings) /block(upload is rejected with a message including specific findings; the temp file is already analyzed inBeforeFileAddedEvent, no FAL entry is created). Thresholdpdf.uploadGateSeverity(error/warning),pdf.uploadGateAdminBypass(admins receive only the warning instead of the block). Frontend form uploads are exempt. - The block message uses
ResultException/ResultMessage— the only way in whichExtendedFileUtilityrenders a custom message text in the upload dialog (a RuntimeException is swallowed into the generic "write permissions" message). - Every backend PDF upload automatically persists a report
(
triggered_by=upload) — the module stays current without manual runs.
[1.0.28]¶
Fixed¶
- PDF module: "Befunde"/"Prüfen" loaded the backend nested in the content frame.
The links were relative
?detail=…URLs without a module route/token — TYPO3 redirected to the backend root. All module links are now generated via the backend router (UriBuilder::buildUriFromRoute). - German module label:
de.locallang_pdf_mod.xlfadded — the module is now named "PDF-Barrierefreiheit" in the German backend instead of "PDF Accessibility".
[1.0.27]¶
Added¶
PDF accessibility (phase 1: inventory check)
- New subsystem
Pdf/:PdfAnalyzerextracts the accessibility-relevant facts in a single parse pass (smalot/pdfparser, pure PHP — runs without Java/binaries even on shared hosting); 9 rules check them against Matterhorn/PDF-UA: tagged PDF, scanned pages without a text layer, document language, document title (+ DisplayDocTitle), figure alt texts, encryption, font embedding, bookmarks, PDF/UA marking. Reuses the audit DTOs (Finding/Severity/Report) → consistent scores and severities. - Backend module "PDF Accessibility" (Media): all FAL PDFs with traffic-light status/score, findings list per file with recommended action and WCAG/PDF-UA reference, single and batch check ("new & changed").
- CLI/Scheduler
nt_ai:pdf-audit(--limit,--force,--fail-on=error|warningfor CI). New tabletx_ntai_pdf_report(one report per file, upsert); unchanged files (same SHA1) are skipped — scheduled runs are practically free. - Planned (phase 2): upload gate (warn/block analogous to the Publishing Quality Gate); phase 3: AI rules, veraPDF connection, dashboard widget, BFSG section.
[1.0.26]¶
Added¶
- Site-wide keyboard guards (WCAG 2.4.1 / 2.4.7) in
a11y-widget.css: skip links with.visually-hidden-focusableare made visible on keyboard focus (many themes don't provide the resolving:focusrule — the link stayed invisible at 1×1 px for sighted keyboard users), and a:focus-visiblesafety net enforces a visible focus outline, even when the theme suppressesoutlineglobally (keyboard only; mouse focus stays theme-styled). Both guards with a dark-mode variant.
[1.0.25]¶
Fixed¶
- Accessibility widget and TTS player marked as
translate="no". The UI has its own i18n; DOM translators (nt_lingua) previously collected and translated their labels along with it (appeared e.g. as "Deficienza di colore" in the cache). Containers now carrytranslate="no"/data-nt-notranslate.
[1.0.24]¶
Added¶
- Concurrent completions (
ConcurrentAiProviderInterface). New optional public capability:completeMany()performs several independent text completions simultaneously (curl_multi inAbstractHttpProvider, implemented for the OpenAI provider). Consumers (e.g. nt_lingua) checkinstanceofand otherwise fall back to sequentialcomplete()calls. Speeds up large batches split into chunks considerably (N-times serial → ~one roundtrip).
[1.0.23]¶
Added¶
- Setting
ntai.a11yWidget.scaleTarget(CSS selector, default#maincontent): determines which element font enlargement and color filters act on. This way a customer theme with a differing content wrapper can set the target itself, without fixed elements (sticky header, back-to-top, widget) being scaled/filtered along with it.
Fixed¶
- Color-deficiency filter now takes effect. The SVG
url()color-matrix filter was on<html>, where Blink/WebKit silently ignore it → no visible effect (contrast/saturation as function filters did take effect). Text size (zoom) and color filters are now applied via JS as an inline style on a configurable content element (instead of on<html>). - Color-deficiency modes are now correction instead of simulation. The matrices (red/green/blue color deficiency) daltonize — they separate confusable colors, instead of simulating the visual impairment; matching the setting text.
[1.0.22]¶
Fixed¶
- Accessibility overlay & TTS player dark in manual dark mode. The
widget chrome dark colors previously only applied under OS dark
(
@media prefers-color-scheme: dark). A newhtml[data-nt-theme="dark"]block ina11y-widget.cssmirrors them, so that the overlay and player also appear dark when dark mode is activated via the widget (including!importantagainst the hover hardening). - No white flash on page change in dark mode. The set's inline config script
now reads
localStorage.ntA11yPrefsbefore the first paint and setsdata-nt-theme="dark"immediately — no more FOUC.
[1.0.21]¶
Added¶
Reusable site set netthinks/nt-ai
- nt-ai now ships its own site set (
Configuration/Sets/NtAiFrontend) with: settings definitions (categories accessibility / read-aloud, prefixntai.*), asset includes (a11y-widget.js/.css,tts.js) and the inline config script (data-nt-*on the<html>). Customer systems now include only one dependencynetthinks/nt-aiin their site configuration — no more manual maintenance ofsettings.definitions.yaml. Site-specific theming (dark-theme colors, tweaks, read-aloud button in the template) stays in the respective sitepackage. - Setting prefix migrated from
netthinks.*tontai.*;TtsMiddlewarereadsntai.tts.*accordingly.
[1.0.20]¶
Added¶
Read-aloud: player control + click-and-read
- Player bar extended: back/forward (paragraph navigation) as well as controls for
volume and speed (cloud live via
playbackRate/volume, Web Speech from the next paragraph or on release). Complements pause/stop. - Click and read as a widget mode: activate and click on a paragraph
to read aloud from there (
window.ntTts.setClickMode).
Fixed¶
- Seek fix: forward/backward seeking no longer read two paragraphs in parallel — the
AbortErrorwhen aborting cloud playback no longer triggers a Web Speech fallback.
[1.0.19]¶
Added¶
- TTS pronunciation replacements. A new site setting to replace terms before
speech output (format
Begriff=Aussprache, several separated by|), e.g.TYPO3=Typo drei. Only the speech output changes, the visible page text stays.
[1.0.18]¶
Added¶
- "Vorlesen" icon in the navbar (next to the language selection) as a freely placeable trigger of the read-aloud feature.
Fixed¶
- Widget hardening against theme bleed: generic
button:hover/a:hoverrules of the theme (white text,::aftereffects, letter spacing) made the icon/options unreadable on hover and caused line breaks. Colors/metrics are now enforced via!important(hover = dark, never white), decorative pseudo-elements neutralized.
[1.0.17]¶
Added¶
Accessibility widget (frontend, stage 1)
- New frontend personalization widget (deliberately not an accessibility overlay):
text size, text spacing (WCAG 1.4.12), contrast, dark mode, reduce
animations, highlight links, more readable font, reading aid (ruler),
hide images. Settings stored locally in
localStorage, no tracking. - No DOM/ARIA rewriting: the preferences act via
data-nt-*attributes on<html>and pure CSS feature layers. The widget itself is fully keyboard/ AT operable (aria-expanded/aria-pressed,Esc, focus return, no focus trap) and respects system settings (prefers-reduced-motion/-contrast/-color-scheme). - Build-free delivery:
a11y-widget.css+a11y-widget.js(classic script). Additionally contains a site-wideprefers-reduced-motionguard for the theme scroll animations (.animate-box) and focus visibility in Windows contrast mode (forced-colors). - Backend-configurable via the site settings (accessibility):
widget on/off, position (4 corners), label on/off. Default: subtle
symbol at the bottom right (does not cover the cookie button). Config reaches the
frontend as
data-nt-a11y-*attributes on the<html>tag (inline script in the<head>; the sitepackage set delivers assets + attributes). - Font-size scaling made more robust via an
!importantCSS rule perdata-nt-fontscale(instead of inline style), index logic corrected (level 0 = neutral). - Widget chrome more subtle (icon launcher, text optional) and in
px/eminstead ofrem, so that it does not grow along with the font scaling itself. - Symbol size and edge margins (horizontal/vertical) configurable via the site settings
(
--nt-a11y-size/-offset-x/-offset-y). - Native dark mode instead of an invert filter: the widget dark mode now switches
html[data-nt-theme="dark"]and thus a real dark theme (nt-dark-theme.cssin the sitepackage; the brand orange remains an accent). First version — fine-tuning of individual sections follows. prefers-contrast: moreis now taken into account site-wide (links underlined, stronger focus/border rendering);forced-colorssupport extended.- Font size via page zoom instead of root
font-size(the theme sizes px-based, so the scaling was ineffective) —zoomon<body>, reflow-friendly. - Position setting as a dropdown (site settings
enum+labels.xlf/de.labels.xlfwith German labels) instead of free text. - Zoom now acts on
#maincontentinstead of<body>— otherwise fixed elements (launcher, back-to-top) were shifted and hero headings (home page/subpages) were enlarged along with it. - The launcher sits freely above the bottom corner (spacing to back-to-top/cookie badge).
- "Hide images" leaves the logo, navigation, hero and footer untouched.
- "Reset everything" additionally as a symbol (↺) at the top of the panel next to the close X.
- Explanatory tooltip on the launcher (hover/focus) with a short description + shortcut hint — important when only the symbol (without a label) is displayed.
- One-time first-visit hint: the tooltip appears automatically on the first visit
and is remembered (
localStorage), no longer on subsequent pages. - Keyboard shortcut Alt + 1 opens/closes the panel; all features are operable by keyboard
(real buttons, focus moves into the panel,
Esccloses with focus return). - New features (EyeAble parity): blue-light filter (warm overlay, intensity control),
color-deficiency filter (red/green/blue color deficiency via SVG color matrix), color saturation/
grayscale (control), larger mouse cursor, mute sound (including later
loaded media), focus highlighting. Contrast gets an intensity control.
Filters (contrast/saturation/color deficiency) are composed as one
--nt-filterchain on<html>; blue light as a separate overlay. - Explanatory text per menu item: on hover/focus, a short description appears at the side of the panel.
Read-aloud (text-to-speech) — ReadSpeaker replacement
- Self-hosted read-aloud feature, hybrid and admin-switchable: Web Speech API
(browser-native voices, free, local) or cloud TTS (premium voices, currently
OpenAI). Reads
#maincontentblock by block with highlighting of the spoken paragraph, a player bar (pause/stop), fully keyboard-operable. - Usable in the widget ("Webseite vorlesen") and freely placeable in the template via
[data-nt-tts]buttons or the Fluid partialReadAloud(e.g. next to the click path). - Cloud audio is cached server-side (
TtsService+ntai_ttscache), the key stays on the server; the client automatically falls back to Web Speech on cloud errors. FE endpoint/nt-ai/ttsviaTtsMiddleware. Config: site settings (engine/rate/scope) + nt-ai ext config (cloud provider/voice/model). - Fix: pause in the player was unreliable (
speechSynthesis.pause()browser-dependent) → now cancel + restart of the current paragraph on "Continue", with a guard against a race. - Fix: oval pause/stop buttons (theme
buttonstyles) hardened. - Cloud TTS in the token/cost dashboard: every real cloud generation (cache miss)
is recorded (
TokenUsageService::trackTts, context "tts", character count in the input field). The pricing table extended by TTS (USD per 1M characters:tts-115,tts-1-hd30,gpt-4o-mini-tts12). The "Token usage" widget shows the row "Vorlesen (TTS, Zeichen)" including estimated costs; cache hits cost nothing. - Cloud TTS endpoint secured: responds only with site engine
cloud, rejects cross-site requests (Sec-Fetch-Site), rate limit per IP (tts.rateLimit, default 30/min., HTTP 429 withRetry-After),X-Robots-Tag: noindex. Cache hits do not count towards the limit. - Documentation comprehensively updated: all widget features (blue light/color deficiency/saturation/ cursor/sound/focus, controls, explanatory texts), symbol selection, read-aloud feature including protection & costs (user-guide + configuration, de/en).
Launcher symbol selectable
- Eight vector-rebuilt accessibility symbols (person in a double circle,
person in a circle, person with arms, person on a line, person in a filled circle,
wheelchair, active wheelchair, hand with person) — selectable in the site settings
(
data-nt-a11y-icon), all single-color (currentColor).
[1.0.16]¶
Added¶
Lighthouse in the page layout
- The AI assistant panel in the page layout module shows a new column
"Lighthouse" with the four measurement rings of the page's most recently saved
measurement (read-only, link into the module). New endpoint
LighthouseAjaxController::latestAction(routent_ai_lighthouse_latest).
Changed¶
- Page-score tips in the backend language. The improvement suggestions of the
AI content-quality score are now generated in the UI language of the backend user
instead of in the language of the checked page (a German backend delivers
German tips even for English pages).
PageScoreService::analyzePage()receives an optionalsuggestionLanguageCode;ScoreAnalysisAjaxControllerpasses the backend language.
[1.0.15]¶
Added¶
- Overall report (cross-page): a new printable BFSG report across all
audited pages — aggregated conformity verdict, key figures including
average score, page overview (worst first) and the most frequent
WCAG barriers with occurrences + affected pages. Button "Gesamtbericht (BFSG)"
in the site overview (
BfsgReporter::renderOverview, routent_ai_audit_report_overview); considers only accessible pages.
[1.0.14]¶
Added¶
- Print-optimized CSS for both BFSG reports:
print-color-adjust: exactpreserves the severity/verdict colors in the PDF, screenshot and snippets are limited, clean page breaks. A dedicated CSP header so that the "Print" button works despite the backend CSP.
[1.0.13]¶
Fixed¶
- BFSG report: findings appeared without text. The reporter iterated the
top-level keys of the
findingscolumn instead of the actual findings list under thefindingskey (the column storesReport::toArray()). Now the message, severity, WCAG group, recommendation and snippet are displayed correctly. - BFSG report: "Print / save as PDF" not working. The backend CSP blocked the inline script; the report response now sets a dedicated CSP header (which the CSP middleware respects).
- Report button showed a broken icon (
actions-file-reportdoes not exist) →actions-file-pdf.
[1.0.12]¶
Added¶
BFSG report & screenshots
- New printable accessibility report (BFSG / EN 301 549 / WCAG 2.2):
findings grouped by WCAG criterion, conformity statement, score +
Lighthouse a11y, screenshot evidence and disclaimer. Button "Bericht (BFSG)" in the
audit module (
BfsgReporter, routent_ai_audit_report), as standalone HTML for printing/saving as PDF. - Lighthouse: the screenshot rendered by PageSpeed Insights
(
fullPageScreenshot, otherwisefinal-screenshot) is now saved (new columntx_ntai_lighthouse_report.screenshot) and displayed as visual evidence in the report.
[1.0.11]¶
Added¶
Publishing Quality Gate
- New
QualityGateHook(DataHandlerprocessDatamapClass): warns about or blocks publishing a page while its last accessibility audit still has findings at or above the configured severity.blockmode vetoeshidden=0in the pre-hook and forces the page to stay unpublished;warnmode shows a flash message after save. - Extension configuration (Audit tab):
audit.qualityGate(off/warn/block),audit.qualityGateSeverity(error/warning),audit.qualityGateScope(onPublish/onSave),audit.qualityGateAdminBypass. Uses the latest stored report — keepaudit.autoOnSaveon so it stays current (fix → save → re-audit → publish passes). nt_ai:audit --fail-on=error|warning— non-zero exit code for CI/CD quality gates.
Audit not-yet-published pages (preview fetch)
- The audit can now render and check pages that are not publicly reachable yet —
future start date, hidden, or expired — so issues are caught before go-live
(which is the whole point of the quality gate).
PageFetcherappends a short-lived, HMAC-signed preview token (AuditPreviewToken); a new frontend middleware (AuditPreviewMiddleware) verifies it and relaxes time/visibility for that single render only. Signed with the site encryption key, so only this installation can mint a valid token; the preview render is not cached. - Module audits and on-save audits use it automatically; CLI:
nt_ai:audit --preview. - The audit module now reports the real reason when a page can't be reached (e.g. "not published yet / hidden") instead of a generic "Audit fehlgeschlagen".
[1.0.2] - 2026-07-09¶
Changed¶
- SEO AI: language instruction in the prompt reinforced — more consistent target language for generated SEO fields.
[1.0.1] - 2026-07-09¶
Added¶
SEO editing
- SEO field assistant in the SEO panel of the Page Layout module. Generate
→ preview → apply buttons for the SEO title, meta description and
focus keyphrase, each derived from the rendered page content and saved to
the matching
pagescolumn via DataHandler. - Inline "KI" buttons in page properties on
pages.seo_title,pages.descriptionandpages.tx_ntai_focus_keyphrase(in addition to the page teaser).AiTextElementnow renders both single-line inputs and textareas; supported fields and prompts live in the sharedPageAiFieldProvider. - Inline image-description button on
sys_file_metadata.description(vision-based caption, complementary to the alt-text button). - Inline teaser button on
pages.abstractthat generates a teaser from the rendered page content.
[1.0.0] - 2026-07-09¶
First public Git release (Composer). Bundles the previously internally developed features (see internal history below) plus the following new subsystems.
Added¶
Score history and sparkline
tx_ntai_page_scoreis now append-only (addedtstampcolumn, removeddeletefromPageScoreRepository::save()). Every analysis run creates a new row so historical data is preserved.PageScoreRepository::getScoreHistory(pageUid, languageUid, limit)— returns the last N runs in chronological order withcrdateandscoresper run.PageScoreRepository::getAverageScores()andgetScoreDistribution()fixed to consider only the latest row per (pid, sys_language_uid) pair (PHP deduplication on uid DESC); with multiple history rows per page the averages were otherwise counted multiple times.PageScoreRepository::countAnalyzedPages()now counts distinct(pid, sys_language_uid)combinations viaGROUP BYinstead of total rows.ScoreAnalysisAjaxController::latestAction()returns ahistoryarray (last 8 runs).inline-audit.js—renderScorePanel()shows a row of coloured sparkline dots (green/amber/red by average score) when ≥ 2 historical runs are available.
Lighthouse monitoring (Google PageSpeed Insights)
- New DB table
tx_ntai_lighthouse_report— stores Lighthouse category scores (Performance, Accessibility, Best Practices, SEO, 0–100) and Core Web Vitals (LCP, CLS × 1 000, INP, FCP, TTFB) per page + language + strategy (mobile/desktop). - New
Lighthouse/LighthouseService— calls the PSI v5 API, parses the JSON response, saves toLighthouseRepository. Requires the analysed URL to be publicly reachable. - New
Lighthouse/LighthouseRepository— save, findLatest, findHistory, getAverageScores, countAnalyzedPages, findAllLatest. - CLI command
nt_ai:lighthouse [pageId] [recursion] [--strategy=mobile|desktop|both]— schedulable, mirrorsnt_ai:analyze-pagesin structure. - New
Dashboard/Widget/LighthouseWidget— four gauge rings (Performance, Accessibility, Best Practices, SEO) showing averages across analysed pages; shows strategy label and page count. - New extension settings tab Lighthouse:
lighthouse.apiKey,lighthouse.strategy. - Dashboard widget
nt_ai_lighthouse(large,dashboard.widget.nt_ai_lighthouse). ConfigurationService::getLighthouseApiKey(),getLighthouseStrategy().
Email alerts (score threshold monitoring)
- New
Service/AlertService— compares latest AI content scores and latest Lighthouse scores against configurable thresholds; sends one summary HTML email per run listing all violating pages. No per-page spam; one mail covers all violations. - CLI command
nt_ai:alert-check— schedulable; designed to run daily after the nightlynt_ai:analyze-pagesandnt_ai:lighthouseruns. - New extension settings tab Alerting:
alerting.enabled,alerting.recipient(comma- separated),alerting.thresholdGeo/Performance/Semantics/Keywords/Accessibilityfor AI scores andalerting.thresholdLighthousePerformance/Accessibility/BestPractices/Seofor Lighthouse. Zero = disabled for that category. ConfigurationService::isAlertingEnabled(),getAlertRecipients(),getScoreThresholds(),getLighthouseThresholds().
AI Content Quality Scoring (Seiten-Score)
- New subsystem
Score/withPageScoreService,PageScoreRepository,ScoreRangeenum. - DB table
tx_ntai_page_score— append-only history (see Score history above). Stores JSON scores for five categories and JSON suggestions list. - Five score categories analysed per page via LLM (rendered HTML as input):
geo(GEO/SEO),performance(page structure),semantics(HTML5/ARIA),keywords(focus keyword density),accessibility(WCAG signals). Scale: 0–100 each. - Focus keyword read from
pages.tx_ntai_focus_keyphrase— the existing SEO field is re-used so no extra editor input is required. - Score thresholds: High ≥ 75 (green), Medium ≥ 50 (amber), Low < 50 (red).
- Suggestions are LLM-written, language-aware (language of the analysed page), max 120 chars each, max 2 per category, only when score < 80.
- AJAX routes
nt_ai_score_analyze(POST) andnt_ai_score_latest(GET) for the inline panel. - CLI command
nt_ai:analyze-pages [pageId] [recursion]— bulk analysis, supports all configured site languages per page; registered as schedulable Scheduler command.
Page Layout module — Seiten-Score panel (3rd inline column)
PageLayoutAuditListenernow renders a third column "Seiten-Score" beside the Barrierefreiheits-Audit and SEO panels.- Displays five SVG gauge rings (52×52 px, colour-coded) with the scores and the improvement suggestions list below.
- "Neu analysieren" button triggers analysis on demand and refreshes the panel.
- Language resolved from the module URL (
?languages[0]=…) for correct per-language results when editors switch language view.
Dashboard — Score widgets
- Five
DoughnutChartWidgetinstances (one per category): display the average score across all analysed pages as a gauge ring — e.g.85 / 100filled 85 % in the category colour. Legend hidden; score is shown in the chart label. - New
PageScoreOverviewWidget(large widget): renders five mini rings with their average scores plus the total count of analysed pages, all in one widget. PageScoreRepository::getAverageScores()andcountAnalyzedPages()power both.
Token usage — Cost estimation
- New
Configuration/ModelPricing.php— built-in USD price table for 35+ models across all supported providers (OpenAI, Anthropic, Gemini, DeepSeek, Mistral, Groq). Keys are model-name prefixes; longest prefix wins, so versioned names likegpt-4o-mini-2024-07-18resolve correctly. - New
CostEstimationService— calculates(input_tokens / 1M × input_price) + (output_tokens / 1M × output_price)in USD; converts to EUR on request; formats as~$0.19or~0,18 €. - Three new extension settings:
tokenTracking.currency—USDorEUR(default:USD).tokenTracking.eurRate— conversion rate (default:0.92).tokenTracking.customPricing— JSON object to override or extend the built-in price table (e.g. private/fine-tuned models).TokenUsageWidgetshows a blue "ca. Kosten" box below the token total. When a model is not in the pricing table a warning with the model name is shown.TokenUsagePerUserWidgetgains a "ca. Kosten" column (per-user × per-model aggregation with separate input/output token queries).
New AI providers (4 added)
- Google Gemini —
GeminiProvider(gemini-2.0-flashdefault). Vision supported. - DeepSeek —
DeepSeekProvider(deepseek-chatdefault). No vision. - Mistral AI —
MistralProvider(mistral-small-latestdefault). Vision onpixtral-large-latest. - Groq —
GroqProvider(llama-3.3-70b-versatiledefault). No vision.
All four providers are configurable via separate Extension Configuration tabs.
New audit rules (15 added since v1.1.0)
Deterministic rules:
| Rule | WCAG | Severity |
|---|---|---|
IframeTitleRule |
4.1.2 | Error |
ButtonTextRule |
2.4.4 | Warning |
DuplicateIdRule |
4.1.1 | Error |
DocumentLandmarkRule |
1.3.1 / 2.4.1 | Warning |
TableHeaderRule |
1.3.1 | Warning |
FocusOutlineRule |
2.4.7 | Warning |
AriaHiddenFocusRule |
4.1.2 | Error |
VideoCaptionsRule |
1.2.2 | Error |
MetaRefreshRule |
2.2.1 | Error |
ReadabilitySentenceLengthRule |
3.1.5 AAA | Notice |
ReadabilityParagraphLengthRule |
3.1.5 AAA | Notice |
ReadabilitySubheadingRule |
3.1.5 AAA | Notice |
SeoTitleLengthRule |
2.4.2 (SEO) | Warning |
SeoMetaDescriptionLengthRule |
SEO | Warning |
SeoKeyphraseRule |
SEO | Error/Warning/Notice |
AI-powered rules:
| Rule | What AI checks |
|---|---|
AiAltTextQualityRule |
Quality of existing alt texts (generic, filename-based, nonsensical) — up to 20 images |
AiHeadingQualityRule |
Descriptiveness of headings — up to 15 headings |
AiMetaDescriptionRule |
Accuracy and clickability of meta description |
Total: 26 audit rules (9 deterministic + 3 AI in v1.1.0 → 23 deterministic + 5 AI now counted as two batches; 3 AI rules in this release).
Fixed¶
- Upload error message suppressed correctly.
GenerateAltTextOnUploadListenerwrapped only the AI call (generateSafe) but left DB operations (language overlayinsert/update) andgetForLocalProcessing()uncovered. Any exception from these paths surfaced as "Upload fehlgeschlagen" in the backend even though the file was stored successfully. The entire processing logic is now wrapped in a top-leveltry/catchin__invoke(); errors are logged and the upload always completes. - Dashboard score widgets showed ~100% for one colour.
ScoreDistributionDataProviderreturned page-count distributions (how many pages fall into each quality tier). With only a few analysed pages all falling in one tier, the ring appeared 100 % one colour. Changed to return the average score as a gauge —data = [avgScore, 100 − avgScore]with colour matching the score value.
Removed¶
- "AI Tools" backend module (
web_ntai) and its playground UI. The AI services are now integrated directly into the editing forms and the SEO panel where they are actually used. The genericnt_ai_generateandnt_ai_summarizeAJAX endpoints remain as the shared backend API; the unusednt_ai_translateendpoint was removed.
Internal pre-release history (May 2026, scheme
1.0.0→1.2.0) — before the public Git releasev1.0.0(July 2026), which delivered the above feature set bundled together. Retained for traceability.
[1.2.0] - 2026-05-24 (intern)¶
Added¶
- TYPO3 v14 LTS compatibility. Tested against TYPO3 v14.0 (released April 2026). CI matrix now covers v12.4, v13.4 and v14.0 against PHP 8.1-8.4.
Changed¶
- Backend module parent auto-detected. In v14, the toplevel module
webwas renamed tocontent(Core feature #107628). The extension detects the running TYPO3 major version and picks the correct parent automatically — no manual change required when upgrading. - Composer constraints widened:
typo3/cms-core: ^14.0,phpunit/phpunit: ^11.0 || ^12.0,typo3/testing-framework: ^10.0.
Fixed¶
- Auto-audit-on-save now actually triggers on regular saves.
Previously the listener was bound to
AfterRecordPublishedEvent, which only fires when records are published from a workspace. The feature never worked in workspace-less installations. The listener now usesAfterDatabaseOperationsEventwith status filtering, so it fires on every backend page save. It also resolvesNEW...placeholder UIDs viaDataHandler::substNEWwithIDsso newly created pages get audited correctly.
[1.1.0] - 2026-05-24 (intern)¶
Added¶
- Accessibility audit module. New backend module "Web -> Accessibility Audit" with score (0-100), severity-grouped findings, WCAG references, AI-generated fix suggestions, CSV export, and history.
- 9 built-in WCAG rules: image alt, heading hierarchy, link text, lang attribute, form labels, page title, inline contrast, AI link suggestions, AI readability check.
- Three audit triggers: manual button, auto-on-save event listener
(with cooldown),
nt_ai:auditCLI command (CI-friendly via--min-score). - Configurable alt-text generation: style (concise/descriptive/detailed), max length, tone, brand context, custom multi-line instructions, "no image-of prefix" enforcement, detected-text transcription toggle.
- Per-call alt-text overrides via
$optionOverridesargument onAiService::generateAltText(). - Defensive post-processing strips quote-wrapping, "Image of"/"Bild von" prefixes, and enforces the length cap on AI output.
- TYPO3-standard documentation in
Documentation/(reST/Sphinx) with introduction, installation, configuration reference, user manual, developer guide, known problems, changelog.
Changed¶
AiService::generateAltText()now accepts a fourth argument$optionOverrides(BC-compatible default[]).- README rewritten for the expanded feature set.
Tests¶
- 56 unit tests, 144 assertions, all green.
- New tests for
AltTextOptions,ContrastCalculator,ImageAltRule,LinkTextRule,Report.
[1.0.0] - 2026-05-23 (intern)¶
Added¶
- Initial release.
- AI providers: Anthropic Claude, OpenAI, Ollama.
- High-level
AiServiceAPI withgenerate,translate,summarize,generateAltText,describeImage. - Backend module "AI Tools" with playground UI.
- AJAX endpoints for backend JavaScript integration.
- CLI command
nt_ai:generate-alt-textsfor batch alt-text generation. - TYPO3 12.4 and 13.4 compatibility.
- Unit tests for
ConfigurationService,PromptBuilder,ProviderRegistry. - GitHub Actions CI for PHP 8.1-8.4 across TYPO3 12/13.
- GPL-2.0-or-later.