Zum Inhalt

Changelog

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) sets lang/dir at 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 watch lang and dir on <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 (container dir, 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:stop with reason: 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 event nt-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.verifyCommand adds an external check (e.g. qpdf --check, or pikepdf with the bundled Resources/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, /Info and /ID of 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 body next to header, main and footer without 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). A div with a role rather than <aside>, so theme styles for aside do 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, alternative and description are nullable with mode useOrOverridePlaceholder: 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 showed alt="" while editors still saw the grey text. Both elements now render the core switch exactly like TextElement; 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-repair for installations that ran 1.31.0 or earlier: lists image usages stored with '', and with --apply resets those to NULL whose file has a text (there '' visibly hides it). --include-without-file-text also resets the others, --field limits it to alternative or description, --list shows 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: restricts hideWhen to a viewport (media query, e.g. (max-width: 767px)); without hideWhen it hides the widget at that viewport alone. Re-evaluated when the viewport changes.
  • nt-tts:clickmode event (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:setup is 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 h2 colour (and font settings), which could lack contrast on the panel background.
  • nt-a11y:open / close fired 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 (#1c5bd8 in 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 settings ntai.a11yWidget.primaryColor / primaryColorDark set 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/-y are documented as override points on .nt-a11y-widget, also in media queries.
  • Formal address by default (B5): German texts use "Sie"; ntai.a11yWidget.address = du switches back. introText / tooltipText replace the two texts per site.
  • Widget events and API (F2): nt-a11y:ready, open, close, change { key, value, previous }, reset; window.ntA11y with open, close, toggle, getPrefs, setPref, hide, show, refresh and registerToggle() for own entries such as "Einfache Sprache" (B6).
  • Read-aloud events (F1): nt-tts:beforecollect (cancelable, with waitUntil(promise)), start with its source (button, widget, click, api), segment, pause, resume, stop with reason, fallback, error. The [data-nt-tts] button now goes through the public window.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> and hidden="until-found" in the reading area are opened before reading and restored on stop. Setting ntai.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.generateOnUploadSource is now backend (only real backend uploads; opening a folder that indexes new files cost API calls before), altText.generateOnUploadLimit is 20. Stored values are kept.
  • nt_ai:generate-alt-texts can be scheduled (still at most --limit files 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.scope takes a list, the first selector present wins; default #maincontent, main, [role=main]. Themes without #maincontent read 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_report per audit.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.retentionDays had 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 /StructTreeRoot resolved 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 other pdf.structure findings can appear where "not tagged" stood before. All PDF reports are rebuilt on the next nt_ai:pdf-audit run (analyzer version 7). A test keeps watching the parser: once it reads /XRefStm itself, 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 by nt_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 task nt_ai:process-queue has 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 CsvUtility with 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 /Marked missing 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. /DescendantFonts is 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.altTextDataPolicy forbids the call anyway. The AJAX route nt_ai_alt_text and the bulk command are unchanged; for them the policy setting and altText.excludePrefixes apply.

Fixed

  • AJAX route nt_ai_alt_text failed 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.uploadGateSeverity are named (three at most), the rest is counted ("+ 2 weitere Befunde").
  • pdf.uploadGateSeverity defaults to error instead of warning. With warnings nearly every PDF trips the gate, and block would 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 with pdf.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 needs pcntl and never applied to uploads.
  • pdf.altTextDataPolicy (all / local / off, default all): controls whether PDF text may go to an AI provider for an alt-text suggestion. local allows Ollama only, off disables it for PDFs. Enforced in one place for buttons, upload and bulk runs.

Fixed

  • altText.excludePrefixes was 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 public AiProviderInterface with 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 a ProviderException (1700000031) naming provider.default and the providers usable right now; completeMany() reports the same condition as one null per 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() and getModel() 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 error event jumped to the next passage at once, and the rejected play() 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 class t3js-clearable. The core only provides clearable() on <input> elements, so form-engine.js threw 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 of ntAiText, 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's Ai service with the extension key nt_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 through LlmServiceManagerInterface and 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 suggest in composer.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-platform 0.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 under nt_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.disabledRules in 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 with role="img" without accessible name, warning), landmark.name (several nav/aside without 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.httpAuthUser and audit.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 and no_index pages; 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). --queue creates 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 tagged nt_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-queue after --keep-days days (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 --force lead 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 setting pdf.auditTimeLimit (tab PDF, default 60 seconds, 0 = off) and option --time-limit N to override it for one run. A file over the limit is recorded as "not auditable" with the reason. With the PHP extension pcntl (CLI) the run continues with the next file; without pcntl, 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 its memory_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()); LighthouseRepository and PageScoreRepository aggregates 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, setting rateLimitPerMinute), 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 (default 4, 0 = off) caps how many cloud generations run at the same time across all visitors. Further new passages get HTTP 503 with Retry-After: 5 before any budget is reserved or the provider is asked; the widget falls back to the browser voice as before. Slots are atomic rows in tx_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 BackendAccess service 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 needs editMeta (a writable file mount), modify rights on sys_file_metadata and the alternative field, 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_limit and 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) and tts.requirePageText (default on).

Changed

  • tts.rateLimit now only counts new passages; cached passages are served without touching the limit, as the setting's label always said.
  • The cache ntai_ratelimit is no longer registered; its tables (cache_ntai_ratelimit*) can be removed in "Analyze Database Structure".

Upgrade notes

  • Run vendor/bin/typo3 extension:setup (new table tx_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_metadata or alternative field 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.requirePageText off 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 ElementMissing object, not null. Its content is false, so ?-> never short-circuits and !== null is always true. PdfAnalyzer read /StructTreeRoot and /MarkInfo in one expression; for every PDF without /MarkInfo the call on ElementMissing threw, the surrounding catch swallowed it, and /Lang, /Outlines and /DisplayDocTitle were never read. pdf.lang and pdf.title therefore reported missing language and title display on files that have both. Reported with a PDF 1.3 without tags; thank you.
  • Tagged PDFs without /MarkInfo counted as untagged — same cause. pdf.tagged reported them, and pdf.uaIdentifier / pdf.imageAlt skipped them.
  • Outlines were reported for every tagged PDF, because a missing /Outlines compared !== null. pdf.bookmarks never 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 !== null pattern, 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.fonts now 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:setup once to add the column. The next nt_ai:pdf-audit run re-parses every PDF once — parsing time only, no API calls. For large stocks, spread it with --limit or --folder.
  • Expect findings to move: false pdf.lang, pdf.title and pdf.tagged findings disappear; pdf.bookmarks and pdf.fonts can 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 raises AfterFileAddedEvent. With backend, 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 default all keeps 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

  • GenerateAltTextOnUploadListener listens to AfterFileAddedEvent as well. The decision which event counts lives in the new AltTextOnUploadPolicy, 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() called imagedestroy() 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 on SYS.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() called curl_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.json allows ^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.json has not allowed for some time.

[1.15.0]

Added

  • Model names can come from the environment. Every model setting — provider.<name>.model for all seven providers, and tts.model — now accepts %env(NAME)%, like the API keys already did. A project can keep model names out of its versioned settings.php and 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() calls removeAll() on the query restrictions — legitimately, because the report table's deleted filter has to live in the JOIN condition — and then re-added the page restrictions by hand. The QueryBuilder's DefaultRestrictionContainer carries 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: AuditCommand and AnalyzePagesCommand do not call removeAll() 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 of AnalyzePagesCommand (findAllNormalPages() and collectChildren()). LighthouseCommand already 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 strict fallback 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 reason column 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 sends Retry-After: 3600 and 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 install on 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. AuditTask kept 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()), and SchedulerTaskRepository::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 its last_checked stays 0 and ORDER BY last_checked ASC picks 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 table tx_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-After is 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_deferral records 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; Run extension:setup once 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 — and docs/ carries export-ignore, so it never reaches an installed instance. Anyone searching vendor/netthinks/nt-ai/ during a fault diagnosis, which is the first place to look, did not find it. It is now in Documentation/Configuration/Index.rst, which is part of the distributed package. Reported from outside, after the section could only be located via git diff against 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:lighthouse on 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.rst stopped at 1.2.0 while the extension was at 1.12.x, which reads as an abandoned project. It now points to the shipped CHANGELOG.md as 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 pageId from a Scheduler task no longer becomes page 0. A task created in the backend stores every argument it was configured with, so an unset pageId arrives as "" rather than null. Both nt_ai:analyze-pages and nt_ai:lighthouse cast that straight to int, and (int)'' === 0: instead of measuring every page, they measured exactly one page that does not exist. The same case was already handled for the strategy option; the argument now gets the same treatment, and a pageId of 0 is rejected with a clear error rather than one unreachable-page warning per language.
  • nt_ai:analyze-pages no 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 pageId is stored at all, that it now means "all pages", and that tasks created from a script should pass arguments: [].

[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 on AiService builds its system prompt through PromptBuilder, 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 past AiService to the provider locator — and everything that does so skips TokenUsageService, 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. $context and $task are 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

  • WidgetSiteScope gained a public options() 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.distinguishable no longer fires a global, unsuppressable error on every page. The rule scans the stylesheet text for a { 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 offending selector { … } declaration instead of the first 300 characters of the (minified) stylesheet. The per-anchor inline text-decoration:none check is unchanged in intent (now a Warning).
  • Individual audit rules can finally be disabled per page tree via TSconfig. The disabledRules mechanism existed but no caller populated it, so the documented TSconfig switch did nothing. AuditService::auditPage() now reads mod.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.distinguishable is 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/ :focus per G183) needs a headless browser and is tracked as a separate feature.

[1.10.1]

Fixed

  • nt_ai:analyze-pages no 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 are doktype = 1 but 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 the SiteFinder, 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 --limit slot any more, so a small --limit reaches real pages instead of being used up by these two page types (0 analyzed). The run summary now reads N 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-only measures 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 latest tx_ntai_lighthouse_report entry is newer than the last change to the page or its content elements — the same MAX(pages.tstamp, MAX(tt_content.tstamp)) comparison as analyze-pages, but per strategy (a both run 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-only is now a shared PageChangeDetector service used by both nt_ai:analyze-pages and nt_ai:lighthouse, instead of being duplicated in the commands. No behavioural change to analyze-pages.

[1.9.0]

Added

  • nt_ai:analyze-pages --changed-only re-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 when audit.aiRules = 0). With the flag a page/language is skipped when its latest tx_ntai_page_score entry is newer than the last change to the page or its content elements — the comparison uses pages.tstamp and MAX(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-audit no longer dies silently on one file and blocks the rest. A memory-heavy PDF exhausted the CLI memory_limit inside smalot/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 N option skips PDFs larger than N MB (also recorded) before parsing, for hosts with a tight memory_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_checked stayed 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. AuditTask now (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>.jpg name, which an editor cannot find in the file list. Findings now display the original file name (resolved via sys_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.editorsMayPublish is 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.pdfPathPrefix site setting (and the global declaration.pdfPathPrefix fallback) 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 the netthinks/nt-ai-declaration set.

[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 on isAdmin(); the new declaration.editorsMayPublish switch (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 /barrierefreiheit slug 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-site ntai.declaration.targetPageUid setting first, then a /barrierefreiheit page that actually belongs to that site. The global declaration.targetPageUid is 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. DeclarationDataService read 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's ntai.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 AuditTask scheduler task is switched off (e.g. during a migration). It is now added only when a non-disabled AuditTask exists in the scheduler, mirroring the existing nt-lingua check.

Added

  • New site set netthinks/nt-ai-declaration carrying 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 in config.yaml.
  • declaration.editorsMayPublish extension setting (see Security).

Changed

  • The assistant, read-aloud and "simple language" measures are no longer pre-filled. declaration.measures now defaults to empty; the accessibility assistant and the read-aloud (TTS) measure are instead auto-derived from whether the netthinks/nt-ai frontend set is actually active for the site (via its ntai.a11yWidget.enabled / ntai.tts.enabled settings). 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 in settings.php and should be cleared once.

[1.6.0]

Fixed

  • The scheduler audit now rotates across the entire page tree. AuditTask sorted the candidate pages by uid ascending, without taking into account when a page was last checked — every run checked the same pages with the lowest uid, and the rest of the tree was never reached (reported with 1,216 pages and limit = 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 on tx_ntai_audit_report. A fixed limit thus covers the complete inventory across multiple runs. The report table's deleted filter is placed in the JOIN condition so that never-checked pages (NULL) are not dropped; the page restrictions (deleted/hidden) are set explicitly.
  • audit.aiRules now 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 with audit.aiRules = 0. AuditService now 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, ntAiImageDescription and ntAiText now call renderFieldInformation(), renderFieldControl() and renderFieldWizard() — in addition to the field's TCA description (which is already rendered via renderLabel()). A description set 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 = user and 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 #maincontent block 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] / the ReadAloud Fluid partial). Ships as tts.js, TtsService and TtsMiddleware; 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 a sys_file_reference with its own alternative/description fields — 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 new FormAiFileResolver resolves both contexts in one path (metadata via file, reference via uid_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

  • searchFields removed from the TCA of tx_ntai_audit_report. TYPO3 14 no longer evaluates the entry in the ctrl section and reports a deprecation at bootstrap; which fields are searchable is now declared as searchable on the respective column. As with the point below, this affects every test suite that runs with failOnDeprecation.
  • ext_emconf.php removed, metadata moved into composer.json. TYPO3 14.3 discards ext_emconf.php and expects instead extra."typo3/cms".version and extra."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 with failOnDeprecation red. The version is deliberately placed in the extra block 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, via get('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 same AiProviderInterface — calling code remains unchanged.

Changed

  • ProviderAdapter no 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.md and docs/developer.en.md referred to the public Netthinks\NtAi\Provider\AiProviderInterface, but showed the methods of the internal Netthinks\NtAi\Service\Provider\AiProviderInterface and AiResult instead of AiResponse as 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; dvh additionally 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: AlertService sent via MailMessage::send(). This method was removed in TYPO3 v14 — MailMessage inherits from Symfony's Email, which does not have send(). As soon as a threshold was actually breached, the send would have aborted with a fatal error. The send now runs via MailerInterface; 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> via page.headerData, but via page.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 FAILURE as 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 about lighthouse.apiKey when 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-Limit under "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 in sys_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 error after 5 s — and error severity is not an option, because it flips the upload response to hasErrors=true and the stored file would be incorrectly marked as "failed". A new pdf-upload-notice.js is loaded once into the backend main frame via AfterBackendPageRenderEvent (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 in BeforeFileAddedEvent, no FAL entry is created). Threshold pdf.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 which ExtendedFileUtility renders 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.xlf added — 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/: PdfAnalyzer extracts 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|warning for CI). New table tx_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-focusable are made visible on keyboard focus (many themes don't provide the resolving :focus rule — the link stayed invisible at 1×1 px for sighted keyboard users), and a :focus-visible safety net enforces a visible focus outline, even when the theme suppresses outline globally (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 carry translate="no" / data-nt-notranslate.

[1.0.24]

Added

  • Concurrent completions (ConcurrentAiProviderInterface). New optional public capability: completeMany() performs several independent text completions simultaneously (curl_multi in AbstractHttpProvider, implemented for the OpenAI provider). Consumers (e.g. nt_lingua) check instanceof and otherwise fall back to sequential complete() 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 new html[data-nt-theme="dark"] block in a11y-widget.css mirrors them, so that the overlay and player also appear dark when dark mode is activated via the widget (including !important against the hover hardening).
  • No white flash on page change in dark mode. The set's inline config script now reads localStorage.ntA11yPrefs before the first paint and sets data-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, prefix ntai.*), asset includes (a11y-widget.js/.css, tts.js) and the inline config script (data-nt-* on the <html>). Customer systems now include only one dependency netthinks/nt-ai in their site configuration — no more manual maintenance of settings.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.* to ntai.*; TtsMiddleware reads ntai.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 AbortError when 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:hover rules of the theme (white text, ::after effects, 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-wide prefers-reduced-motion guard 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 !important CSS rule per data-nt-fontscale (instead of inline style), index logic corrected (level 0 = neutral).
  • Widget chrome more subtle (icon launcher, text optional) and in px/em instead of rem, 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.css in the sitepackage; the brand orange remains an accent). First version — fine-tuning of individual sections follows.
  • prefers-contrast: more is now taken into account site-wide (links underlined, stronger focus/border rendering); forced-colors support extended.
  • Font size via page zoom instead of root font-size (the theme sizes px-based, so the scaling was ineffective) — zoom on <body>, reflow-friendly.
  • Position setting as a dropdown (site settings enum + labels.xlf/de.labels.xlf with German labels) instead of free text.
  • Zoom now acts on #maincontent instead 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, Esc closes 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-filter chain 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 #maincontent block 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 partial ReadAloud (e.g. next to the click path).
  • Cloud audio is cached server-side (TtsService + ntai_tts cache), the key stays on the server; the client automatically falls back to Web Speech on cloud errors. FE endpoint /nt-ai/tts via TtsMiddleware. 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 button styles) 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-1 15, tts-1-hd 30, gpt-4o-mini-tts 12). 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 with Retry-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 (route nt_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 optional suggestionLanguageCode; ScoreAnalysisAjaxController passes 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, route nt_ai_audit_report_overview); considers only accessible pages.

[1.0.14]

Added

  • Print-optimized CSS for both BFSG reports: print-color-adjust: exact preserves 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 findings column instead of the actual findings list under the findings key (the column stores Report::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-report does 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, route nt_ai_audit_report), as standalone HTML for printing/saving as PDF.
  • Lighthouse: the screenshot rendered by PageSpeed Insights (fullPageScreenshot, otherwise final-screenshot) is now saved (new column tx_ntai_lighthouse_report.screenshot) and displayed as visual evidence in the report.

[1.0.11]

Added

Publishing Quality Gate

  • New QualityGateHook (DataHandler processDatamapClass): warns about or blocks publishing a page while its last accessibility audit still has findings at or above the configured severity. block mode vetoes hidden=0 in the pre-hook and forces the page to stay unpublished; warn mode 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 — keep audit.autoOnSave on 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). PageFetcher appends 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 pages column via DataHandler.
  • Inline "KI" buttons in page properties on pages.seo_title, pages.description and pages.tx_ntai_focus_keyphrase (in addition to the page teaser). AiTextElement now renders both single-line inputs and textareas; supported fields and prompts live in the shared PageAiFieldProvider.
  • Inline image-description button on sys_file_metadata.description (vision-based caption, complementary to the alt-text button).
  • Inline teaser button on pages.abstract that 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_score is now append-only (added tstamp column, removed delete from PageScoreRepository::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 with crdate and scores per run.
  • PageScoreRepository::getAverageScores() and getScoreDistribution() 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 via GROUP BY instead of total rows.
  • ScoreAnalysisAjaxController::latestAction() returns a history array (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 to LighthouseRepository. 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, mirrors nt_ai:analyze-pages in 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 nightly nt_ai:analyze-pages and nt_ai:lighthouse runs.
  • New extension settings tab Alerting: alerting.enabled, alerting.recipient (comma- separated), alerting.thresholdGeo/Performance/Semantics/Keywords/Accessibility for AI scores and alerting.thresholdLighthousePerformance/Accessibility/BestPractices/Seo for Lighthouse. Zero = disabled for that category.
  • ConfigurationService::isAlertingEnabled(), getAlertRecipients(), getScoreThresholds(), getLighthouseThresholds().

AI Content Quality Scoring (Seiten-Score)

  • New subsystem Score/ with PageScoreService, PageScoreRepository, ScoreRange enum.
  • 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) and nt_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)

  • PageLayoutAuditListener now 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 DoughnutChartWidget instances (one per category): display the average score across all analysed pages as a gauge ring — e.g. 85 / 100 filled 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() and countAnalyzedPages() 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 like gpt-4o-mini-2024-07-18 resolve correctly.
  • New CostEstimationService — calculates (input_tokens / 1M × input_price) + (output_tokens / 1M × output_price) in USD; converts to EUR on request; formats as ~$0.19 or ~0,18 €.
  • Three new extension settings:
  • tokenTracking.currency — USD or EUR (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).
  • TokenUsageWidget shows 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.
  • TokenUsagePerUserWidget gains 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-flash default). Vision supported.
  • DeepSeek — DeepSeekProvider (deepseek-chat default). No vision.
  • Mistral AI — MistralProvider (mistral-small-latest default). Vision on pixtral-large-latest.
  • Groq — GroqProvider (llama-3.3-70b-versatile default). 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. GenerateAltTextOnUploadListener wrapped only the AI call (generateSafe) but left DB operations (language overlay insert/update) and getForLocalProcessing() 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-level try/catch in __invoke(); errors are logged and the upload always completes.
  • Dashboard score widgets showed ~100% for one colour. ScoreDistributionDataProvider returned 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 generic nt_ai_generate and nt_ai_summarize AJAX endpoints remain as the shared backend API; the unused nt_ai_translate endpoint was removed.

Internal pre-release history (May 2026, scheme 1.0.0→1.2.0) — before the public Git release v1.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 web was renamed to content (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 uses AfterDatabaseOperationsEvent with status filtering, so it fires on every backend page save. It also resolves NEW... placeholder UIDs via DataHandler::substNEWwithIDs so 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:audit CLI 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 $optionOverrides argument on AiService::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 AiService API with generate, translate, summarize, generateAltText, describeImage.
  • Backend module "AI Tools" with playground UI.
  • AJAX endpoints for backend JavaScript integration.
  • CLI command nt_ai:generate-alt-texts for 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.