Context
The product specification "Website Check: Fehlende Alt-Texte" asks for a
list of every use of an image that has no alternative text, across pages
and records, reached from the dashboard. Each hit is fixed with the chat or
marked as decorative. It is a work list, not a guided tour.
ADR-023: The chat guides a process did not adopt site-wide scan tiles and the widget Website
prüfen, because no scanner existed. This ADR adds one scanner, for alt
texts, and the widget for it. Bulk operations ("Website bearbeiten") stay
not adopted.
What exists: alt texts live on the image reference
(sys_file_reference.alternative) and, as the file's default, in the
file list (sys_file_metadata.alternative). TYPO3 has no field for a decorative
image. nr-llm's set_file_alternative_text writes sys_file_metadata,
which the specification rules out: an alt text belongs to one use of the
image, not to the file.
Decision
Slices. The feature ships in three:
- The check, the "decorative image" field, the list Fehlende Alt-Texte
and its two entries on the dashboard (this ADR's subject). No button in
the list writes anything.
- The write tool, and Optimieren: the chat opens beside the list with
the hit's context, proposes an alt text, saves it or marks the image
decorative, and moves to the next hit.
- The privacy setting Bildanalyse: whether a scaled image is sent to
the model, and folders that are never sent.
Where the write tool lives. Writing alternative on an existing
reference goes into nr-llm, next to set_file_alternative_text: every
content write the chat makes is an nr-llm tool with its approval card
(ADR-004: nr-llm as LLM Abstraction Layer, ADR-021: Editors approve their own changes in chat), through DataHandler, so permissions,
workspaces and the record history apply, and the tool is useful without
this extension. Setting tx_nrmcpagent_decorative is a tool of this
extension, registered with nr-llm's tool registry like the guided-state
tools (ADR-020: Guided processes report progress and focus): nr-llm does not know the field, and the field goes
away with this extension. It writes through DataHandler as well. Whether it
asks for approval is slice 2's decision.
The decorative field. sys_file_reference.tx_nrmcpagent_decorative
(the extension's prefix; the specification's tx_aiassistant_ is an
example), a checkbox, default off, from ext_tables.sql and a TCA
override that puts it after Alternative text in imageoverlayPalette.
Like Alternative text it is an exclude field: an editor sees it once
their group is granted it. No l10n_mode, so each language version of a
reference has its own. It is not on sys_file_metadata.
Frontend output. fluid_styled_content's image partial
(Media/Rendering/Image.html in 13.4, Image.fluid.html in 14.3, the
same markup) gets an override that renders a decorative image with
f:image … alt="" and every other image with the core's own line.
f:media cannot be used for the decorative branch: it treats alt=""
as missing and falls back to the file's alternative text; f:image
keeps an explicit empty one. The partial path sits at key 5: above the
core's (0), below a site package's (10, styles.templates.partialRootPath).
The path is never added to every site. A content rendering of its own
with its image partial below key 5 would lose that partial for every
image: bootstrap_package 16 sets its own path at 0 and, at 10, a setting
whose default names a folder it does not ship (Partials/ContentElement/;
its partials are in Partials/ContentElements/), so key 5 would win and
its images would lose their <picture> and srcset output. The path
comes only together with fluid_styled_content:
- Sites with site sets opt in with the set
netresearch/nr-mcp-agent-decorative-images, which depends on
typo3/fluid-styled-content. A defaultContentRendering addition
would not reach them at all: the core adds it before the sets, and
fluid_styled_content's TypoScript starts with lib.contentElement >.
- Sites with
sys_template records get it right after
fluid_styled_content's static template
(addTypoScript(…, 'fluidstyledcontent/Configuration/TypoScript/')),
and only when they include that template. bootstrap_package 16.0.1
ships site sets only, no static template, and its sets do not depend
on typo3/fluid-styled-content.
No further mechanism is added; the remaining setups are documented in the
installation chapter and covered by the rendering test:
- fluid_styled_content loaded with
@import in a sys_template
record: the static-template key is read only for Include static
entries, so the site imports the set's setup.typoscript after
fluid_styled_content's.
- The static template followed by another content rendering that
redefines
lib.contentElement without > (bootstrap_package 15
through its static template): key 5 stays and replaces that
rendering's partial, so the site removes it with
lib.contentElement.partialRootPaths.5 >.
- bootstrap_package or any other own content rendering never takes the
set or the import; it adds the condition to its own image partial.
A site package with its own image partial at a higher key keeps it; the
installation documentation gives the condition to add.
Which fields count. Every column of type file in the TCA, read at
runtime: a newly installed extension's image fields are checked without
configuration (the specification asked whether this is possible: it is;
13.4 migrates the older inline FAL fields to type file). Of these, the
file must be an image (sys_file.type). Excluded by default, in
altTextExcludedFields: pages.og_image,
pages.twitter_image, be_users.avatar and backend_layout.icon
(no image with an alt text in the frontend), and pages.nav_icon:
bootstrap_package renders it with its icon ViewHelper, which outputs no
alt text from the reference. Unlike the
specification's "pages = content elements and Medien", every other image
field of pages counts too, thumbnail among them: bootstrap_package
renders it through the image partial with the reference's alt text, in
thumbnail menus. Tables are excluded with
altTextExcludedTables (none by default).
Which tab a hit belongs to. A reference is followed to its record and,
through inline fields that name their owner (foreign_field), to that
record's owner, up to four levels. The outermost record decides: a page or
a content element puts the hit under Seiten, any other table under its
own tab, labelled with the table's TCA title. So a carousel item's image
belongs to its content element on the page, and a content element inside
a news record to that record. An excluded table also takes the records it
owns with it.
When a hit is open. The image's effective alt text is empty: the
reference's own text, or, where the reference has none (NULL), the
file list's text in the reference's language where the file has a metadata
translation, else in the default language. The file list's text is read in
its live version; a draft in a workspace does not decide outside it. A
reference whose text was overridden with an empty one is open even if the
file list has a text, because the frontend then renders it empty;
whitespace counts as empty. A reference for all languages
(sys_language_uid -1) is checked against the default language's file
list text only; on a translated page the frontend shows that language's
metadata translation instead, so such a reference can be listed although
a translation has a text, or the other way round.
Not decorative; reference, records on the way and page not deleted. Only
references in a field the record's type shows: listed in its showitem,
directly or in a palette, and not turned into another field type by
columnsOverrides. A reference left in a field after the record's type
changed (an image on a Text element that was Text & Images) is in no
form and in no output. Fields a plugin adds through subtypes_addlist
are not read; a reference in such a field is not checked.
Hidden and timed content counts and carries why it is not visible: a
parent page that passes its visibility on to its subpages
(extendToSubpages) and is hidden, not started yet or expired; the
page; its translation in the content's language; the element or record; a
start time in the future, an end time passed; and, beyond the
specification, a hidden reference (Bild ausgeblendet). The level highest
up wins, a parent page before the page. Content in a language for which
the page has no translation gets no reason, although the frontend may not
show it.
Permissions. A hit is kept only when the user may change the
reference, as DataHandler would allow it: readPageAccess() with edit
page for a page property and edit content otherwise (records: their
folder), which includes the web mounts; no editlock on the page or a
record on the way (admins excepted); tables_modify for every table on
the way and for sys_file_reference; the image field, the inline fields
on the way and sys_file_reference.alternative granted where they are
exclude fields; the language of the reference and of each record. The
same filter feeds the list, its tabs and the dashboard number. Not
enforced in this slice: per-value access to the content type
(explicit_allowdeny) and access checks extensions add to DataHandler
through hooks; DataHandler still applies both when slice 2 writes.
Workspaces. In a workspace each row is overlaid with its workspace
version: an alt text or mark set there, a file list text set there, and a
record deleted there, count; references new in the workspace are found.
Records of tables that are not workspace-aware are left out. Not covered:
an alt text that is set live and emptied only in the workspace, on the
reference or in the file list, is not found, because the query decides on
the live rows; a metadata translation that exists only in the workspace is
not read. The tests load EXT:workspaces for this part.
Live, no cache. The list and the dashboard number run the query and
the filter on every request, and the list pages in memory. The
specification's nightly cache for large sites is not built; it is added
when a site needs it. The query over the references has no limit and the
number is not counted in SQL: whether a hit counts depends on the page
access over the rootline, the field and language rights and the record
type, which are decided per reference in PHP. A limit would make the
dashboard number wrong. Its cost grows with the number of open references,
not with the number of pages.
The list and its entries. A second route of the existing module
nr_mcp_agent_chat, reached from the widget Website prüfen (always
listing Fehlende Alt-Texte) and a tile in Für deine Website empfohlen
(shown only with open hits). Both are behind the same gate as the other
assistant widgets, ChatToolbarItem::checkAccess() (ADR-009: Group-Based Access Control via Extension Configuration):
every action of the feature leads into the chat. Optimieren is left out
until slice 2 rather than shown disabled: a disabled button cannot be
focused, so its explanation would not reach keyboard users.