mindfulmarkup/mindfula11y

Find and fix accessibility issues in TYPO3: viewhelpers for semantic headings and landmarks, missing image alt detection and generation with AI (ChatGPT). A backend module for quick remediation. More features to come.

Installs: 246

Dependents: 0

Suggesters: 0

Security: 0

Stars: 3

Watchers: 0

Forks: 0

Open Issues: 0

Type:typo3-cms-extension

pkg:composer/mindfulmarkup/mindfula11y

v0.8.1 2025-09-12 15:12 UTC

README

The WFA Accessibility Toolkit is a TYPO3 extension that integrates accessibility tools directly into the TYPO3 backend, helping editors and integrators improve the accessibility of their content.

Installation

Install via Composer:

composer require mindfulmarkup/mindfula11y

Features

  • Alternative Text Checker: Backend module that lists all sys_file_reference records (e.g., images) without alternative text, making it easy to find and fix missing alt attributes.

    Screenshot of the accessibility backend module listing images missing an alternative text with an input field, a generate and a save button.

  • AI-Powered Alt Text Generation: Supports generating alternative texts for images using ChatGPT.

  • Viewhelpers and TCA columns for heading types and landmarks: Various viewhelpers and a new accessibility tab for content elements to make it easy for editors to use accessible heading types and create ARIA landmarks.

  • Heading Structure Overview: Backend module that visualizes the heading structure of the selected page and allows editors to easily identify issues and edit heading types for records using the custom ViewHelper.

    Screenshot of the accessibility backend module showing a heading tree with an error shown due to a skipped heading level

  • Landmark Structure Overview: Backend module that displays ARIA landmarks on the selected page. Editors can review landmark structure, identify accessibility issues, and edit landmark roles directly from the module to improve page navigation and semantic structure.

    Screenshot of the landmark structure of a page in the accessibility module showing an error due to a duplicated "search" landmark sharing the same label.

Planned Features

  • Automated Accessibility Scanners: Integration of remote accessibility testing tools to review more issues directly in the backend.

Extension Settings

You can configure Mindful A11y in the extension settings:

  • OpenAI API Key: Set your OpenAI API key for ChatGPT-powered features.
  • Chat Model: Choose the OpenAI model for generating alternative text (e.g., gpt-4o-mini, gpt-4o).
  • Image Detail: Set the detail level for image analysis (low or high).
  • Disable Alt Text Generation: Option to disable the AI-powered alternative text generation. Also inactive if no OpenAI API key is set.

Page TSconfig Options

Configure module behavior per page using Page TSconfig (Configuration/page.tsconfig):

mod {
    mindfula11y_accessibility {
        missingAltText {
            enable = 1
            ignoreColumns {
                # Do not include column `image` from table `tt_content` in the alternative text check.
                tt_content = image
            }
        }
        headingStructure {
            enable = 1
        }
        landmarkStructure {
            enable = 1
        }
    }

    web_layout {
        mindfula11y {
            # Hide mindfula11y accessibility info in page module
            hideInfo = 0
        }
    } 
}

Finding images with missing alternative text

The Accessibility module provides a list of sys_file_reference records that do not have alternative text set. This helps editors identify and fix images that are missing accessibility information.

Backend Module Integration

The Missing Alt Text checker is integrated into the main Accessibility backend module and provides filtering options based on:

  • Current Page: Results are shown for the currently selected page in the page tree
  • Record Type: Filter by specific tables (e.g., tt_content, pages) or show all record types
  • Language: Filter results by specific languages
  • Recursive Depth: Choose how many page levels to scan (1, 5, 10, or 99 levels deep)

Inline Editing

Each missing alternative text entry provides:

  • Image Preview: Thumbnail display with link to view the full-size image
  • Alternative Text Input: Multi-line text area for entering descriptive text
  • AI Generation: When ChatGPT is configured, a "Generate" button automatically creates alternative text
  • Save Functionality: Individual saving for each image
  • Record Link: Direct access to edit the original record containing the file reference

Configuration Options

The module can be configured using Page TSconfig to exclude specific columns:

mod {
    mindfula11y_missingalttext {
        enable = 1
        ignoreColumns {
            # Exclude specific columns from the alternative text check
            tt_content = image,media
            pages = media
        }
    }
}

Heading Types

The Mindful A11y extension adds a tx_mindfula11y_headingtype column to the tt_content table, allowing editors (with appropriate permissions) to set the semantic heading type for content elements. This enables precise control over the document's heading hierarchy for better accessibility.

What are Heading Types?

Heading types define the semantic level and HTML element used to render headings in your content. Proper heading structure is crucial for accessibility, as screen readers and other assistive technologies use headings to navigate and understand the page structure.

Available Heading Types

The extension provides the following heading type options:

  • H1-H6: Semantic heading levels (<h1> through <h6>) for proper document structure
  • Paragraph (p): For text that should be rendered as a paragraph rather than a heading (<p>)
  • Generic div (div): For content that needs custom styling without any semantic meaning (<div>)

Backend Module Integration

Headings are displayed in a tree structure within the Accessibility backend module, providing editors with a clear overview of the page's heading hierarchy. The module identifies accessibility issues such as missing H1 elements or skipped heading levels, and allows for easy editing of heading types directly from the tree view.

Using the Heading ViewHelpers in Fluid Templates

The Mindful A11y extension provides three ViewHelpers for rendering accessible, semantically correct headings in your Fluid templates:

Arguments for Heading and Landmark ViewHelpers

1. HeadingViewHelper (<mindfula11y:heading>) – Main/Standalone Headings

Arguments:

  • recordUid (int, optional): The UID of the record with the heading.
  • recordTableName (string, optional, default: tt_content): Database table name of the record with the heading.
  • recordColumnName (string, optional, default: tx_mindfula11y_headingtype): Name of field that stores the heading type.
  • type (string, optional): The heading type to use (h1, h2, ..., h6, p, div, etc.). If not provided, the value will be fetched from the database record or set to "h2".
  • relationId (string, optional): The relation identifier for this heading (used for referencing in sibling/descendant headings).

Renders a heading element (e.g., <h2>, <h3>, <p>) for a content record or static content. The tag is determined by the stored heading type, a provided type, or a default. Optionally, you can provide a relationId to allow descendant or sibling headings to reference this heading's type.

Basic usage for tt_content records:

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<mindfula11y:heading
    recordUid="{data.uid}"
    recordTableName="tt_content"
    recordColumnName="tx_mindfula11y_headingtype"
    type="{data.tx_mindfula11y_headingtype}">
    {data.header}
</mindfula11y:heading>
</html>

Note: If the type parameter is provided to the viewhelper, it uses this value directly and avoids an additional database lookup. This can improve performance by reducing unnecessary queries.

Static heading (no editing, no DB lookup):

<mindfula11y:heading type="h2">Static heading</mindfula11y:heading>

2. DescendantViewHelper (<mindfula11y:heading.descendant>) – Child/Incremented Headings

Arguments:

  • ancestorId (string, required): The relationId of the heading ancestor.
  • levels (int, optional, default: 1): How many levels to increment the heading type.
  • type (string, optional): The heading type to use. If provided, overrides the computed tag and adds levels.
  • recordUid, recordTableName, recordColumnName (optional): These arguments should only be used if the referenced ancestor (by ancestorId) appears after this ViewHelper in the template, or if the cache lookup does not work. In normal usage, prefer using ancestorId.

Renders a heading as a descendant of a referenced ancestor heading, incrementing the heading level as needed. The ancestor is referenced by ancestorId (which must match a relationId set on a parent heading). The tag is computed by incrementing the ancestor's heading level by the levels argument (default: 1).

Usage:

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<mindfula11y:heading
    recordUid="{data.uid}"
    recordTableName="tt_content"
    recordColumnName="tx_mindfula11y_headingtype"
    type="{data.tx_mindfula11y_headingtype}"
    relationId="mainHeading">
    Parent heading
</mindfula11y:heading>

<mindfula11y:heading.descendant
    ancestorId="mainHeading"
    levels="1">
    Child heading
</mindfula11y:heading.descendant>
</html>

Behavior notes:

  • If the ancestor is a semantic heading (h1h6), the descendant will be the incremented heading level (e.g., h2h3). If the increment would exceed h6, the descendant becomes a paragraph (p).
  • You may override the computed tag by supplying a type argument to the descendant ViewHelper.
  • The ancestor must appear before the descendant in the template for the reference to work. If not, provide the type or recordX arguments directly.

3. SiblingViewHelper (<mindfula11y:heading.sibling>) – Sibling Headings at the Same Level

Arguments:

  • siblingId (string, required): The relationId of the heading sibling.
  • type (string, optional): The heading type to use. If provided, overrides the computed tag.
  • recordUid, recordTableName, recordColumnName (optional): These arguments should only be used if the referenced sibling (by siblingId) appears after this ViewHelper in the template, or if the cache lookup does not work. In normal usage, prefer using siblingId.

Renders a heading at the same level as a referenced sibling heading. The sibling is referenced by siblingId (which must match a relationId set on another heading). The tag is determined by the cached heading type of the sibling.

Usage:

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<mindfula11y:heading
    recordUid="{data.uid}"
    recordTableName="tt_content"
    recordColumnName="tx_mindfula11y_headingtype"
    type="{data.tx_mindfula11y_headingtype}"
    relationId="mainHeading">
    First heading
</mindfula11y:heading>

<mindfula11y:heading.sibling siblingId="mainHeading">
    Sibling at same level
</mindfula11y:heading.sibling>
</html>

Behavior notes:

  • The referenced sibling must appear before this ViewHelper in the template for the reference to work. If not, provide the type or recordX arguments.
  • You may override the computed tag by supplying a type argument to the sibling ViewHelper.
  • If the referenced sibling is not a heading, the same tag is used.

Extending Custom Record Types

To add heading type support to custom record types, follow these steps:

1. Add TCA Column Definition

Create a TCA override file for your custom table (e.g., Configuration/TCA/Overrides/tx_myext_records.php):

<?php
declare(strict_types=1);

use MindfulMarkup\MindfulA11y\Enum\HeadingType;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

// Add the heading type column to your custom table
ExtensionManagementUtility::addTCAcolumns(
    'tx_myext_records',
    [
        'headingtype' => [
            'exclude' => true,
            'label' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.headingType',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'default' => HeadingType::H2->value,
                'items' => [
                    ['label' => HeadingType::H1->getLabelKey(), 'value' => HeadingType::H1->value],
                    ['label' => HeadingType::H2->getLabelKey(), 'value' => HeadingType::H2->value],
                    ['label' => HeadingType::H3->getLabelKey(), 'value' => HeadingType::H3->value],
                    ['label' => HeadingType::H4->getLabelKey(), 'value' => HeadingType::H4->value],
                    ['label' => HeadingType::H5->getLabelKey(), 'value' => HeadingType::H5->value],
                    ['label' => HeadingType::H6->getLabelKey(), 'value' => HeadingType::H6->value],
                    ['label' => HeadingType::P->getLabelKey(), 'value' => HeadingType::P->value],
                    ['label' => HeadingType::DIV->getLabelKey(), 'value' => HeadingType::DIV->value],
                ],
            ],
        ],
    ]
);

// Add the field to your record type's interface
ExtensionManagementUtility::addToAllTCAtypes(
    'tx_myext_records',
    'headingtype',
    '',
    'after:title'
);

2. Update Database Schema

Add the database column to your ext_tables.sql:

CREATE TABLE tx_myext_records (
    headingtype varchar(10) DEFAULT 'h2' NOT NULL
);

Note: In TYPO3 13+, database schema updates are handled automatically based on TCA configuration, so manual ext_tables.sql definitions are no longer required for TCA-defined fields.

3. Use in Fluid Templates

In your Fluid templates, use the ViewHelper with your custom table:

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<mindfula11y:heading 
    recordUid="{record.uid}" 
    recordTableName="tx_myext_records" 
    recordColumnName="headingtype" 
    type="{record.headingtype}">
    {record.title}
</mindfula11y:heading>
</html>

This integration allows your custom records to benefit from the same heading structure analysis and inline editing capabilities provided by the Accessibility backend module.

Landmarks

The Mindful A11y extension adds landmark-related columns to the tt_content table, allowing editors to define ARIA landmarks for better page navigation. These landmarks help screen reader users understand the structure and purpose of different page sections.

What are Landmarks?

ARIA landmarks identify the purpose of different sections of a page, making it easier for assistive technology users to navigate content. They provide semantic meaning to regions of a page and allow users to quickly jump between different sections.

Available Landmark Types

The extension provides the following landmark options:

  • None: No landmark role applied
  • Region: Important Section (only if none of the other options apply)
  • Navigation: Navigation Menu (<nav>)
  • Complementary: Sidebar / Related Content (<aside>)
  • Main: Main Content Area (<main>)
  • Banner: Site Header (<header>)
  • Contentinfo: Site Footer (<footer>)
  • Search: Search Function (<search>)
  • Form: Form Section (<form>)

Landmark Columns

The extension adds three columns to support landmarks:

  • tx_mindfula11y_landmark: The landmark type/role
  • tx_mindfula11y_arialabelledby: Checkbox to use the content element's header as the landmark name
  • tx_mindfula11y_arialabel: Custom landmark name (when not using the header)

Backend Module Integration

Landmarks are displayed in a hierarchical layout within the Accessibility backend module, providing editors with a clear overview of the page's landmark structure. The module identifies many accessibility issues and allows for easy editing of landmark roles and names directly from the structure view.

Using the Landmark ViewHelper

Arguments:

  • recordUid (int, optional): The UID of the record that is being rendered.
  • recordTableName (string, optional, default: tt_content): Database table name of the record being rendered.
  • recordColumnName (string, optional, default: tx_mindfula11y_landmark): Name of field that stores the role.
  • role (string, optional): The landmark role value.
  • tagName (string, optional): Override the HTML tag name regardless of the role. The role attribute will still be applied.

To apply landmarks in the frontend, use the provided LandmarkViewHelper. The ViewHelper renders the appropriate HTML element based on the landmark type and integrates with the backend module for inline editing capabilities.

The ViewHelper automatically selects semantic HTML elements for each landmark role:

  • navigation<nav>
  • main<main>
  • banner<header>
  • contentinfo<footer>
  • complementary<aside>
  • search<search>
  • form<form>
  • region<section>

You can override the automatically selected tag using the optional tagName argument. When tagName is provided, the ViewHelper will use that element and add the appropriate role attribute.

Basic Usage for tt_content Records

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<!-- If aria-labelledby is active and a header is set prefer over aria-label. Change based on your requirements. -->
<f:if condition="{data.tx_mindfula11y_arialabelledby} && {data.header}">
    <f:then>
        <f:variable name="ariaAttributes" value="{labelledby: 'c{data.uid}-heading'}" />
    </f:then>
    <f:else if="{data.tx_mindfula11y_arialabel">
        <f:variable name="ariaAttributes" value="{label: data.tx_mindfula11y_arialabel}" />
    </f:else>
    <f:else>
        <f:variable name="ariaAttributes" value="{}" />
    </f:else>
</f:if>

<mindfula11y:landmark 
    recordUid="{data.uid}" 
    recordTableName="tt_content" 
    recordColumnName="tx_mindfula11y_landmark" 
    role="{data.tx_mindfula11y_landmark}" 
    aria="{ariaAttributes}">
    {data.bodytext}
</mindfula11y:landmark>
</html>

Extending Custom Record Types

To add landmark support to custom record types, follow these steps:

1. Add TCA Column Definitions

Create a TCA override file for your custom table (e.g., Configuration/TCA/Overrides/tx_myext_records.php):

<?php
declare(strict_types=1);

use MindfulMarkup\MindfulA11y\Enum\AriaLandmark;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

// Add the landmark columns to your custom table
ExtensionManagementUtility::addTCAcolumns(
    'tx_myext_records',
    [
        'landmark' => [
            'exclude' => true,
            'label' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.landmark',
            'description' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.landmark.description',
            'onChange' => 'reload',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'default' => AriaLandmark::NONE->value,
                'items' => [
                    ['label' => AriaLandmark::NONE->getLabelKey(), 'value' => AriaLandmark::NONE->value],
                    ['label' => AriaLandmark::REGION->getLabelKey(), 'value' => AriaLandmark::REGION->value],
                    ['label' => AriaLandmark::NAVIGATION->getLabelKey(), 'value' => AriaLandmark::NAVIGATION->value],
                    ['label' => AriaLandmark::COMPLEMENTARY->getLabelKey(), 'value' => AriaLandmark::COMPLEMENTARY->value],
                    ['label' => AriaLandmark::MAIN->getLabelKey(), 'value' => AriaLandmark::MAIN->value],
                    ['label' => AriaLandmark::BANNER->getLabelKey(), 'value' => AriaLandmark::BANNER->value],
                    ['label' => AriaLandmark::CONTENTINFO->getLabelKey(), 'value' => AriaLandmark::CONTENTINFO->value],
                    ['label' => AriaLandmark::SEARCH->getLabelKey(), 'value' => AriaLandmark::SEARCH->value],
                    ['label' => AriaLandmark::FORM->getLabelKey(), 'value' => AriaLandmark::FORM->value],
                ],
            ],
        ],
        'aria_labelledby' => [
            'exclude' => true,
            'label' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.ariaLabelledby',
            'description' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.ariaLabelledby.description',
            'displayCond' => 'FIELD:landmark:!=:',
            'onChange' => 'reload',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
                'default' => 1,
                'items' => [['label' => '']],
            ],
        ],
        'aria_label' => [
            'exclude' => true,
            'label' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.ariaLabel',
            'description' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.columns.mindfula11y.ariaLabel.description',
            'displayCond' => 'FIELD:landmark:!=:',
            'config' => [
                'type' => 'input',
                'size' => 50,
                'max' => 255,
                'eval' => 'trim',
                'required' => true,
            ],
        ],
    ]
);

// Add landmark palette
$GLOBALS['TCA']['tx_myext_records']['palettes']['landmarks'] = [
    'label' => 'LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.palettes.landmarks',
    'showitem' => 'landmark, --linebreak--, aria_labelledby, aria_label'
];

// Add the accessibility tab with landmarks palette
ExtensionManagementUtility::addToAllTCAtypes(
    'tx_myext_records',
    '--div--;LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.tabs.accessibility, --palette--;LLL:EXT:mindfula11y/Resources/Private/Language/Database.xlf:ttContent.palettes.landmarks;landmarks'
);

2. Update Database Schema

Add the database columns to your ext_tables.sql:

CREATE TABLE tx_myext_records (
    landmark varchar(15) DEFAULT '' NOT NULL,
    aria_labelledby tinyint(1) DEFAULT 1 NOT NULL,
    aria_label varchar(255) DEFAULT '' NOT NULL
);

Note: In TYPO3 13+, database schema updates are handled automatically based on TCA configuration, so manual ext_tables.sql definitions are no longer required for TCA-defined fields.

3. Use in Fluid Templates

In your Fluid templates, use the ViewHelper with your custom table:

<html
	xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
	xmlns:mindfula11y="http://typo3.org/ns/MindfulMarkup/MindfulA11y/ViewHelpers"
	data-namespace-typo3-fluid="true"
>
<!-- If aria-labelledby is active and a header is set prefer over aria-label. Change based on your requirements. -->
<f:if condition="{record.aria_labelledby} && {record.title}">
    <f:then>
        <f:variable name="ariaAttributes" value="{labelledby: 'record-{record.uid}-title'}" />
    </f:then>
    <f:else if="{record.aria_label">
        <f:variable name="ariaAttributes" value="{label: record.aria_label}" />
    </f:else>
    <f:else>
        <f:variable name="ariaAttributes" value="{}" />
    </f:else>
</f:if>

<mindfula11y:landmark 
    recordUid="{record.uid}" 
    recordTableName="tx_myext_records" 
    recordColumnName="landmark" 
    role="{record.landmark}" 
    aria="{ariaAttributes}">
    {record.content}
</mindfula11y:landmark>
</html>

This integration allows your custom records to benefit from the same landmark structure analysis and inline editing capabilities provided by the Accessibility backend module.

License

This project is licensed under the GNU General Public License v2.0 (GPL-2.0). See the LICENSE file for details.