Configuration¶
All configuration lives in the extension configuration: Admin Tools → Settings → Extension Configuration → nt_ai.
You can also override values per environment via the standard TYPO3 mechanism in config/system/settings.php:
For secrets, use environment-variable placeholders directly in the extension configuration form:
provider.anthropic.apiKey = %env(NT_AI_ANTHROPIC_KEY)%
provider.openai.apiKey = %env(NT_AI_OPENAI_KEY)%
nt-ai resolves these at runtime via getenv().
Model names from the environment¶
Model names accept the same placeholder. A project can then keep them out of its versioned config/system/settings.php and switch to another model by changing an environment variable, without a commit or a deployment:
// config/system/settings.php (excerpt)
'EXTENSIONS' => [
'nt_ai' => [
'provider' => [
'openai' => [
'apiKey' => '%env(NT_AI_OPENAI_KEY)%',
'model' => '%env(AI_MODEL_OPENAI)%',
],
],
],
],
This works for the model setting of every provider and for tts.model. The whole value must be the placeholder, and the variable name may contain A–Z, 0–9 and _. The variable has to be visible to PHP's getenv() — set by the web server or container, or loaded from .env by a package such as helhum/dotenv-connector.
One deliberate difference to API keys: if the variable is empty or missing, the model falls back to the provider default listed below, not to an empty string. A missing API key marks a provider as unavailable, which is correct. An empty model name would instead be sent to the provider and fail every request, so a forgotten ENV entry must not switch off the AI features. The same fallback applies to a model field saved empty in the backend.
Available since 1.15.0. Literal model names keep working unchanged.
Provider settings¶
provider.default selects which provider is used when none is specified at call time. One of: anthropic, openai, gemini, deepseek, mistral, groq, ollama.
Anthropic Claude¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.anthropic.apiKey |
string | (empty) | API key from console.anthropic.com. Required to use Claude. Supports %env(VAR)% placeholders. |
provider.anthropic.model |
string | claude-sonnet-4-5 |
Model identifier. Examples: claude-opus-4-5, claude-haiku-4-5. Vision is supported on all Claude 3+ and 4+ models. Supports %env(VAR)%, see Model names from the environment. |
provider.anthropic.baseUrl |
string | https://api.anthropic.com/v1 |
Override only if you proxy Anthropic through your own infrastructure. |
OpenAI¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.openai.apiKey |
string | (empty) | API key from platform.openai.com. Required to use OpenAI. |
provider.openai.model |
string | gpt-4o-mini |
Examples: gpt-4o, gpt-4o-mini, gpt-4-turbo. Use a vision model (gpt-4o, gpt-4o-mini) if you want alt texts to work. Supports %env(VAR)%, see Model names from the environment. |
provider.openai.baseUrl |
string | https://api.openai.com/v1 |
Override for Azure OpenAI, OpenRouter, LM Studio, or any other OpenAI-API-compatible endpoint. |
Google Gemini¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.gemini.apiKey |
string | (empty) | API key from aistudio.google.com. Required to use Gemini. |
provider.gemini.model |
string | gemini-2.0-flash |
Examples: gemini-2.5-pro, gemini-2.0-flash, gemini-1.5-flash. All listed models support vision. Supports %env(VAR)%, see Model names from the environment. |
provider.gemini.baseUrl |
string | https://generativelanguage.googleapis.com/v1beta |
Override for proxy setups. |
DeepSeek¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.deepseek.apiKey |
string | (empty) | API key from platform.deepseek.com. Required to use DeepSeek. |
provider.deepseek.model |
string | deepseek-chat |
deepseek-chat = DeepSeek-V3 (general), deepseek-reasoner = R1 (chain-of-thought). Neither model supports vision. Supports %env(VAR)%, see Model names from the environment. |
provider.deepseek.baseUrl |
string | https://api.deepseek.com/v1 |
Override for proxy setups. |
Mistral AI¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.mistral.apiKey |
string | (empty) | API key from console.mistral.ai. |
provider.mistral.model |
string | mistral-small-latest |
Examples: mistral-large-latest, pixtral-large-latest (vision). Vision is only available on pixtral-* models. Supports %env(VAR)%, see Model names from the environment. |
provider.mistral.baseUrl |
string | https://api.mistral.ai/v1 |
Override for proxy setups. |
Groq¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.groq.apiKey |
string | (empty) | API key from console.groq.com. |
provider.groq.model |
string | llama-3.3-70b-versatile |
Examples: llama-3.1-8b-instant, gemma2-9b-it. Groq does not support vision. Supports %env(VAR)%, see Model names from the environment. |
provider.groq.baseUrl |
string | https://api.groq.com/openai/v1 |
Override for proxy setups. |
Ollama (local)¶
| Setting | Type | Default | Description |
|---|---|---|---|
provider.ollama.baseUrl |
string | http://localhost:11434 |
URL of your Ollama instance. Use the in-container URL if you run TYPO3 and Ollama in separate containers (e.g. http://host.docker.internal:11434). |
provider.ollama.model |
string | llama3.2 |
Model name as installed locally. For vision features, install a multimodal model first: ollama pull llama3.2-vision Supports %env(VAR)%, see Model names from the environment. |
AI foundation extensions (optional)¶
Since 1.22.0. Instead of one of the built-in providers, nt_ai can hand its requests to an AI foundation extension. API keys, the model, budgets and request logging are then managed there; the provider.* settings above are not used.
provider.default |
Requires | Configured in |
|---|---|---|
aim |
b13/aim |
AiM backend module (provider configurations) |
nr_llm |
netresearch/nr-llm |
nr_llm backend module (default configuration) |
A bridge exists only while its extension is installed, and it counts as available only when that extension has a usable provider configured.
Limits: bulk runs go through a bridge one request at a time; a request carries one image; image requests through nr_llm (0.40) bypass its budget and telemetry; the token widget can estimate costs only for models in nt_ai's own price table. Both foundations are pre-1.0 — after updating one of them, test an alt-text generation before relying on scheduled runs.
Behavior¶
| Setting | Type | Default | Description |
|---|---|---|---|
behavior.timeout |
integer (seconds) | 60 |
Hard timeout for any AI request. Increase for very slow local models. |
behavior.maxTokens |
integer | 2048 |
Maximum tokens generated per response. Applies to all providers. |
behavior.logRequests |
boolean | off | When on, every AI call is logged via the standard TYPO3 logger. Useful for debugging, but may write sensitive prompt content to log files. Leave off in production. |
behavior.defaultLanguage |
string (ISO 639-1) | en |
Used when no language is specified at call time. Affects content generation, translations, and alt texts. |
Alt-text customization¶
These settings shape the alt texts the AI produces so they match your site's voice and accessibility policy. The settings apply to every alt-text generation, including batch CLI runs and the "Generate alt text" buttons.
| Setting | Type | Default | Description |
|---|---|---|---|
altText.customInstructions |
multi-line text | (empty) | Free-form additional rules for the AI. One instruction per line. Appended verbatim to the system prompt. |
altText.maxLength |
integer | 125 |
Soft cap on alt text length in characters. Set to 0 to disable. The AI is instructed to stay under the limit, and the output is also hard-cut on word boundaries if the model exceeds it. WCAG/screen-reader best practice is around 125 characters. |
altText.style |
enum | concise |
concise — one short sentence. descriptive — 1–2 sentences. detailed — 2–3 sentences, suitable for charts and content images. |
altText.forbidImagePrefix |
boolean | on | Strips openings like "Image of…", "Picture showing…", "Bild von…" from the prompt and defensively from the response. Screen readers already announce "Graphic", making these prefixes redundant. |
altText.includeDetectedText |
boolean | on | The AI transcribes visible text inside the image (signs, screenshots, labels, product packaging). Important for accessibility — text in images is otherwise lost to screen reader users. |
altText.tone |
string | neutral, factual |
Free-form tone description. Examples: friendly, casual (blogs), marketing, enthusiastic (landing pages), technical, precise (documentation). |
altText.brandContext |
string (max 500 chars) | (empty) | A short note about your site or brand sent with every alt-text request. Useful for context-sensitive descriptions. |
Building a strict alt-text policy — German museum site
altText.style = descriptive
altText.maxLength = 200
altText.tone = neutral, sachlich
altText.brandContext = Museum für moderne Kunst. Bildbeschreibungen für ein Fachpublikum, Künstlernamen nur wenn klar erkennbar.
altText.customInstructions:
Bei Kunstwerken: Stil, Material und ungefähre Epoche nennen.
Personen nur als "Besucher", "Mitarbeiter" o.ä. bezeichnen.
Texttafeln und Beschriftungen wörtlich übernehmen.
Alt text on upload¶
| Setting | Type | Default | Description |
|---|---|---|---|
altText.generateOnUpload |
boolean | off | Generate an alt text as soon as a new image arrives. Synchronous: an upload takes about 2–5 seconds longer. Only the default language is filled; translations come from the nt-lingua metadata translation. |
altText.generateOnUploadSource |
enum | all |
Which new files count, see below. |
altText.generateOnUploadLimit |
integer | 0 (no limit) |
The most alt texts per request or scheduler run. Caps the cost when many files arrive at once. Files beyond the limit are left without an alt text and logged once as a warning; generate them later in bulk with vendor/bin/typo3 nt_ai:generate-alt-texts. |
TYPO3 reports a new file in two ways, and altText.generateOnUploadSource decides which one counts:
all— every newly indexed file, the behaviour before 1.16.0. Besides uploads this includes files TYPO3 only finds on disk: copied there by SFTP or rsync and then picked up by the "update storage index" scheduler task, or as soon as someone opens the folder in the file list. Each of those files costs an API call.backend— only files uploaded in the backend. Files copied onto the server, imports on the command line and uploads from frontend forms are left alone.
Use backend wherever files reach the server by other means than the backend.
Audit settings¶
| Setting | Type | Default | Description |
|---|---|---|---|
audit.enabled |
boolean | on | Master switch for the Accessibility Audit backend module. |
audit.autoOnSave |
boolean | off | When on, an audit is automatically triggered every time a page is saved in the backend. Slows down the save action because the page is fetched over HTTP. |
audit.autoCooldown |
integer (seconds) | 120 |
Minimum gap between auto-audits for the same page. Prevents audit storms when editors rapid-save. |
audit.aiRules |
boolean | on | Whether to run AI-powered audit rules. When off, only deterministic rule-based checks run. AI rules are also automatically skipped when no provider is configured. |
audit.retentionDays |
integer (days) | 90 |
Audit reports older than this are eligible for deletion via the standard TYPO3 garbage-collection task. 0 disables. |
audit.maxReportsPerPage |
integer | 10 |
Maximum number of reports kept per page. Older reports are pruned automatically after each new audit run. |
audit.httpAuthUser |
string | empty | Since 1.21.0. User name for sites behind HTTP Basic Auth (staging). The audit uses it when fetching the site's own pages. Also %env(NAME)%. |
audit.httpAuthPassword |
string | empty | Since 1.21.0. The password; preferably as %env(NAME)%. Only sent to the site itself. |
audit.explainPrompt |
multi-line text | (built-in) | Custom prompt template for AI-generated finding explanations. Available placeholders: {ruleId}, {severity}, {message}, {wcagReference}, {snippet}. |
Accessibility statement¶
Fields for the "Erklärung" (Statement) tab in the audit module, which turns audit data into a draft accessibility statement (see user guide → "Generate an accessibility statement").
| Setting | Type | Default | Description |
|---|---|---|---|
declaration.targetPageUid |
integer | 0 |
Target page the statement is published to. 0 = the page with slug /barrierefreiheit automatically. |
declaration.organisation |
string | (empty) | Organisation name used in the statement. Empty = website title from the site configuration. |
declaration.feedbackContact |
text | (empty) | Contact for accessibility feedback (name, email, phone). Mandatory field of the statement. |
declaration.enforcementBody |
text | (empty) | Competent enforcement / conciliation body (for companies under the BFSG usually the market surveillance authority of the federal states). |
declaration.enforcementUrl |
string | (empty) | Link to the competent body (optional). |
declaration.measures |
text | (built-in) | Measures already implemented, one per line or separated by \|. Installed sibling extensions (nt-lingua) and the nt-ai audit are appended automatically. |
Publishing Quality Gate¶
Warns about open accessibility findings when publishing a page, or blocks publishing until critical issues are fixed. The gate uses the page's latest stored audit report, so keep audit.autoOnSave enabled to keep it current. The loop is: fix → save (re-audit) → report updates → publishing passes.
| Setting | Type | Default | Description |
|---|---|---|---|
audit.qualityGate |
options (off/warn/block) |
off |
warn: flash warning on publish. block: prevents publishing (page stays hidden) until the gate passes. |
audit.qualityGateSeverity |
options (error/warning) |
error |
Lowest severity that trips the gate. error = errors only, warning = errors and warnings. |
audit.qualityGateScope |
options (onPublish/onSave) |
onPublish |
onPublish: only when making the page visible (hidden=0). onSave: on every save of a non-hidden page. |
audit.qualityGateAdminBypass |
boolean | on | Administrators may publish despite findings (they only get a warning). |
For pipelines: nt_ai:audit --fail-on=error (or warning) returns a non-zero exit code when findings exist — usable as a CI/CD gate.
PDF accessibility (upload gate)¶
Checks PDFs on backend upload for accessibility (tagged PDF, text layer, document language, title, alternative texts …) and warns or blocks depending on the mode. Regardless of the mode, every backend PDF upload stores a report for the Media → PDF Accessibility module. Frontend form uploads are exempt. Rule details: user guide.
| Setting | Type | Default | Description |
|---|---|---|---|
pdf.uploadGate |
options (off/warn/block) |
off |
warn: the file is stored and the editor gets a persistent warning listing the findings. block: the upload is rejected with a message including the findings — no file is created. |
pdf.uploadGateSeverity |
options (error/warning) |
error |
Severity threshold. warning = errors and warnings (e.g. missing document title), error = critical errors only. |
pdf.uploadGateAdminBypass |
boolean | on | Administrators may upload despite findings (they only get the warning instead of a block). |
pdf.auditTimeLimit |
int (seconds) | 60 |
Since 1.19.0. Maximum time per file in the inventory check nt_ai:pdf-audit (scheduler task). A file over the limit is recorded as "not auditable" and only checked again once it changes; the run ends with exit code 1 and states the count in its summary. With the PHP extension pcntl (CLI) the run continues with the next file; without pcntl it ends at that point and resumes behind the file next time. 0 = no limit. Override per run with --time-limit N. Does not apply to the upload gate and the module (max_execution_time applies there). |
pdf.verifyCommand |
string | empty | Since 1.33.0. Additional structure check of a PDF changed by the quick fix before it replaces the original. Called with the path of the working copy as last argument; the fix is refused when the copy fails the check (exit code ≠ 0) while the original passes — warnings the original already has do not block it. Empty = built-in check only (read back and compare). Examples: qpdf --check, or Python with pikepdf and the bundled script Resources/Private/Scripts/pdf-check.py (see user guide, quick fix). |
pdf.uploadMaxSizeMb |
int (MB) | 25 |
Since 1.23.0. Larger PDFs are not audited during the upload so the request cannot hang (the time limit of the inventory check only works on the command line). The editor gets a notice; the inventory check picks the file up on its next run. 0 = no limit. |
pdf.altTextDataPolicy |
options (all/local/off) |
all |
Since 1.23.0. An AI alt text for a PDF sends the title and up to 8,000 characters of document text to the AI provider. local allows that with Ollama only, off disables the feature for PDFs. Applies to buttons, upload and bulk runs. Images are not affected. Single folders are excluded with altText.excludePrefixes — since 1.23.0 for the buttons as well. |
Since 1.23.0 error is the default for pdf.uploadGateSeverity. An installation that has the value stored keeps it; only new installations and configurations never saved get the new default. The upload notice names errors first, then — if the threshold is warning — warnings, three at most, and counts the rest. A PDF with at least one error scores at most 49 out of 100.
For inventory checks and pipelines: nt_ai:pdf-audit [--limit N] [--force] [--fail-on=error|warning] [--time-limit N]
— pure PHP (no system binaries required). Each file is attempted exactly once per content (SHA1) and version of the check — a failed or aborted attempt counts too; an older version of the check or --force trigger a new check.
Token tracking and cost estimation¶
| Setting | Type | Default | Description |
|---|---|---|---|
tokenTracking.enabled |
boolean | off | When on, every AI call is recorded in tx_ntai_token_usage. Required for the Token Usage dashboard widgets. |
tokenTracking.currency |
options | USD |
Display currency for estimated costs. USD → ~$0.19, EUR → ~0,18 €. |
tokenTracking.eurRate |
float | 0.92 |
USD → EUR conversion rate. Only used when currency is set to EUR. |
tokenTracking.customPricing |
text (JSON) | (empty) | Override or extend the built-in model price table. Prices in USD per 1,000,000 tokens. Prefix matching applies — "gpt-4o" matches gpt-4o-2024-08-06. |
{
"my-fine-tuned-gpt4o": {"input": 4.00, "output": 12.00},
"custom-ollama-model": {"input": 0.00, "output": 0.00}
}
Built-in price table (as of 2026-07)¶
| Model prefix | Provider | Input $/1M | Output $/1M |
|---|---|---|---|
gpt-4o-mini |
OpenAI | 0.15 | 0.60 |
gpt-4o |
OpenAI | 2.50 | 10.00 |
claude-haiku-4 |
Anthropic | 0.80 | 4.00 |
claude-sonnet-4 |
Anthropic | 3.00 | 15.00 |
claude-opus-4 |
Anthropic | 15.00 | 75.00 |
gemini-2.0-flash |
0.10 | 0.40 | |
gemini-2.5-pro |
1.25 | 10.00 | |
deepseek-chat |
DeepSeek | 0.27 | 1.10 |
mistral-small |
Mistral AI | 0.20 | 0.60 |
llama-3.3-70b |
Groq | 0.59 | 0.79 |
The full table is in EXT:nt_ai/Configuration/ModelPricing.php. Unknown models are shown as a warning in the dashboard widget. TTS models (tts-1, tts-1-hd, gpt-4o-mini-tts) are listed there priced per 1M characters.
Read-aloud (TTS)¶
Engine, rate and read scope are chosen in the site settings (category "Vorlesen (TTS)"). The following apply to the cloud engine only and live in the extension configuration (tab "Vorlesen (TTS)"):
If the endpoint refuses a passage (403 text is not on the page, 429 rate limit, 503 budget or busy, 502 provider error), the browser voice reads it since 1.22.2; the next passage is then requested from the cloud again. After two refusals in a row the browser voice reads the rest of the page without further requests. Pause, resume, speed and volume act on the voice that is currently reading.
Two cases in which the page-text check always refuses passages:
- Content loaded by JavaScript (e.g. a list fetched by Ajax): the check compares against the HTML the server delivers, which does not contain such passages. They are read by the browser voice.
- Sites behind HTTP Basic Auth (staging): the check fetches the page itself and gets a 401 unless
audit.httpAuthUserandaudit.httpAuthPasswordare set (since 1.21.0) — the read-aloud check uses the same credentials as the audit.
| Setting | Type | Default | Description |
|---|---|---|---|
tts.cloudProvider |
options | openai |
Premium voice provider. API key = that provider's key (tab "Provider settings"). |
tts.voice |
string | alloy |
Provider voice (OpenAI: alloy, echo, fable, onyx, nova, shimmer). |
tts.model |
string | tts-1 |
TTS model (OpenAI: tts-1, tts-1-hd, gpt-4o-mini-tts). Supports %env(VAR)%, see Model names from the environment. |
tts.rateLimit |
integer | 30 |
Max new cloud requests per IP per minute (bot/mass-usage protection). 0 = unlimited; cache hits don't count. Changing IPs gets around this limit, the daily budgets not. |
tts.dailyCharacterBudget |
integer | 200000 |
Characters per calendar day (UTC) sent to the provider, across all visitors. Reserved atomically before every call; when used up → HTTP 503 until the next day. 0 = no global limit (not recommended). With OpenAI tts-1 the default is roughly 3 USD per day. |
tts.dailyRequestBudget |
integer | 2000 |
Provider calls per calendar day (UTC), across all visitors. 0 = no global limit (not recommended). |
tts.maxConcurrent |
integer | 4 |
Cloud generations that may run at the same time, across all visitors (since 1.18.0). Further new passages get HTTP 503 with Retry-After: 5 at once and are read with the browser voice. A slot of an aborted request is freed after the request timeout plus 30 s. Cache hits don't count. 0 = no limit. |
tts.requirePageText |
boolean | 1 |
Only speak text that occurs on the requested page of this site. The public rendering of the page is fetched once and kept for an hour. Limits: see the User Guide. |
The cloud endpoint /nt-ai/tts only answers when the site engine is cloud; it rejects cross-site requests and is rate-limited. Generated audio is cached server-side, so repeat reads of the same text are free. See User Guide → Read-aloud.
Backend permissions¶
Since 1.17.0 every view, export and AJAX action of nt-ai applies the same rules as the TYPO3 core. Administrators are not restricted.
- Pages: reports, scores and Lighthouse results are only shown for pages the editor may show and that lie inside his webmounts, and only for languages he may edit. Site overview, totals, rule aggregation, overview report, the Lighthouse tab and the dashboard widgets are built from these pages only. "Alle Websites" means all sites the editor can see; the site selector only lists those. A site chosen in the selector or the URL is a filter, never a permission.
- Files: generating an alt text needs read access to the file (file mount). Saving an alt text from the audit needs the right to edit the metadata: a writable (not read-only) file mount, modify rights on
sys_file_metadataand itsalternativefield, and access to the default language. The value is written through DataHandler (history, workspaces). Without these rights nt-ai answers 403 and changes nothing. Only the default-language metadata record is written; translated alt texts are no longer overwritten. - PDF module and PDF widget: list, totals, detail view and check actions only cover PDFs inside the editor's file mounts.
- Accessibility declaration (since 1.18.0): it is a legal statement about a whole site, so it is never built from partial data. An editor (with
declaration.editorsMayPublish) may see its facts, generate and publish it only with access to the site's root page and to every page with audit or Lighthouse results of that site, every language with results and every PDF the declaration counts (file mounts). Otherwise the tab explains that full access to the site is required and the AJAX actions answer 403. All figures — including the Lighthouse average — only cover the selected site and its configured languages, for administrators too. - Dashboard widgets (since 1.18.0): audit score, Lighthouse, page score and the score charts filter by the editor's languages as well as his pages.
- AI budget (since 1.18.0): every backend action that calls a paid provider (audit run, explain, alt text and image description, SEO and form texts, score analysis, declaration) draws from one per-user budget,
rateLimitPerMinute(default 30 per minute). Switching between actions does not reset it; when it is used up the action answers HTTP 429 before the provider is called.
Lighthouse monitoring¶
Public URLs required
The PSI API can only analyse publicly accessible URLs. Use this feature on staging or production servers, not on local DDEV instances — Google cannot reach *.ddev.site URLs.
| Setting | Type | Default | Description |
|---|---|---|---|
lighthouse.apiKey |
string | (empty) | Google API key from console.cloud.google.com (enable the "PageSpeed Insights API"). Without a key, requests are rate-limited to ~25 per 100 seconds. For production use, always configure a key. Supports %env(NT_AI_LIGHTHOUSE_KEY)%. |
lighthouse.strategy |
options | mobile |
Device profile for Lighthouse. mobile (default), desktop, or both (runs two PSI calls per page — doubles quota usage). |
# All pages, mobile strategy (reads setting from Extension Configuration).
vendor/bin/typo3 nt_ai:lighthouse
# Page 42 and full subtree, desktop only.
vendor/bin/typo3 nt_ai:lighthouse 42 99 --strategy=desktop
# Page 1 children, run both strategies.
vendor/bin/typo3 nt_ai:lighthouse 1 1 --strategy=both
Results are stored in tx_ntai_lighthouse_report as append-only history. The Lighthouse-Scores dashboard widget shows the average scores across all analysed pages.
Score threshold alerting¶
| Setting | Type | Default | Description |
|---|---|---|---|
alerting.enabled |
boolean | off | Master switch. No email is ever sent when this is off. |
alerting.recipient |
string | (empty) | One or more email addresses, comma-separated. Example: webmaster@example.com, seo@example.com. |
alerting.thresholdGeo |
integer (0–100) | 0 |
Alert when the GEO/SEO AI score for any page falls below this value. 0 disables alerting for this category. |
alerting.thresholdPerformance |
integer (0–100) | 0 |
Threshold for the Performance AI score. |
alerting.thresholdSemantics |
integer (0–100) | 0 |
Threshold for the Semantics AI score. |
alerting.thresholdKeywords |
integer (0–100) | 0 |
Threshold for the Keywords AI score. |
alerting.thresholdAccessibility |
integer (0–100) | 0 |
Threshold for the Barrierefreiheit AI score. |
alerting.thresholdLighthousePerformance |
integer (0–100) | 0 |
Threshold for the Lighthouse Performance score. |
alerting.thresholdLighthouseAccessibility |
integer (0–100) | 0 |
Threshold for the Lighthouse Accessibility score. |
alerting.thresholdLighthouseBestPractices |
integer (0–100) | 0 |
Threshold for the Lighthouse Best Practices score. |
alerting.thresholdLighthouseSeo |
integer (0–100) | 0 |
Threshold for the Lighthouse SEO score. |
Recommended Scheduler setup — run nt_ai:alert-check after the analysis commands so it reads fresh data:
03:00 nt_ai:analyze-pages (AI content scores)
03:30 nt_ai:lighthouse (Lighthouse PSI scores)
04:00 nt_ai:alert-check (compare + send email if needed)
Which language an audit covers¶
A page is audited in the language the task is set to, by building that
language's URL for the default-language UID. Translated page records are
therefore not separate targets and are skipped: as its own candidate such a
record yields the translated slug without the language prefix — /about-us/
instead of /en/about-us/ — a URL that does not exist and can only answer 404.
To cover more than one language, set up one task per language. They share the deferral table, so they do not duplicate each other's knowledge.
Up to and including 1.13.1
Translated records were selected as candidates. Every one answered 404, consuming a slot per run and — before 1.13.0, when the pause did not survive a run — potentially filling the batch entirely. On a bilingual site this affected roughly half of all candidates. After updating they leave the rotation on their own; leftover deferral entries expire by themselves.
Pages without a translation
Whether a page with no translation answers 404 or renders the default
language depends on the site's fallback configuration. With strict it
answers 404 and is set aside for a week as a result; with a fallback it is
audited as rendered.
Pages that are not currently published — hidden, or outside their start/end time — are not candidates either. A page whose end time has passed answers 404 like a hidden one and never becomes auditable again by itself; a page with a future start time joins the rotation on its own once it goes live. Auditing a page deliberately before publication is what the signed preview token is for. It is valid only for the one page (language, site, URL path) it was minted for.
Pages the audit cannot reach¶
The scheduled audit works through the page tree least-recently-audited first. A page it cannot fetch writes no report, so its "last checked" stays empty and it would be picked first again on the very next run — a handful of unreachable pages can therefore occupy every slot and stop the rotation entirely.
To prevent that, such a page is set aside for a while. How long depends on what the server answered:
- 404 or 410 — an intentional "not found", typically a detail URL without a record. Set aside for a week: it is a result, not a failure.
- 503 or 429 — "temporarily unavailable", i.e. a maintenance page or a rate
limit. Set aside for the period the server asks for in its
Retry-Afterheader, or one hour when it does not send one. A site in maintenance therefore needs no further configuration. - Anything else (HTTP 5xx, timeouts, connection errors) counts as a technical failure. The pause grows with each consecutive failure — 6 hours, 24 hours, 3 days, then 7 days. A successful audit clears the record.
Only a genuine technical failure counts towards the rule that turns the task red when every auditable page in a batch failed.
These records live in tx_ntai_audit_deferral (one row per page and language,
with the reason and the time the pause ends):
SELECT page_uid, status_code, failure_count,
FROM_UNIXTIME(deferred_until) AS paused_until, reason
FROM tx_ntai_audit_deferral ORDER BY page_uid;
Entries are removed automatically once their pause has elapsed.
The reason column also states where the pause came from — Retry-After: 1800s,
no Retry-After, default, clean 404/410 or the position in the failure
escalation. Without it a row cannot answer the question: a server sending
Retry-After: 3600 and the built-in one-hour fallback produce the very same
timestamp.
Before 1.13.0
This state was held on the Scheduler task object, where it could not survive
a run: the scheduler saves a task before it executes and writes back only
its execution details, never its parameters. The pause therefore never took
effect. Nothing needs to be configured — but extension:setup has to run
once so the new table is created.
Optional arguments in a Scheduler task¶
A task created through the TYPO3 backend stores every argument of the
command, including the ones you left blank — an unset pageId is saved as an
empty string, not as "absent". Both nt_ai:analyze-pages and nt_ai:lighthouse
treat an empty pageId as "not given" and fall back to all pages, so leaving
the field empty in the backend does what it looks like it does.
Two related notes:
- A
pageIdof0is rejected with an error. Page 0 is the tree root, never a real page: it belongs to no site and has no URL, so it can only ever produce unreachable results. - If you create tasks from a script rather than the backend, pass
arguments: []so no empty values are stored in the first place.