Documents must have <title> element to aid in navigation
The page title is the first thing a screen reader announces and the text shown in browser tabs and bookmarks. Without it, blind customers and anyone with several tabs open cannot tell whether they are on the product page, the cart or the checkout.
- Rule id
- document-title
- Source
- axe-core 4.13.0
- Impact
- Serious
- Fix tier
- AUTO
- on Shopify, Shopware, WooCommerce and Magento
- Status
- runs
What the check does
Ensure each HTML document contains a non-empty <title> element
The rule tests only the top-level document in which axe-core was started; the titles of documents inside an iframe are not tested.
The check reads document.title, the title as the browser reports it at that moment, not the raw HTML. Validiscan’s scanner starts axe-core after waiting, up to a time limit, for the page to load and its network to go quiet, so a title that a script sets before then is the one tested. The text is trimmed, with non-breaking spaces counted as spaces; if anything is left, the page passes. A missing <title> and one holding only whitespace both fail. The evaluation returns a plain true or false, so there is no needs-review outcome.
Nothing about the wording is measured, and titles are not compared between pages. The same “Home” title on every template passes, as does the shop name alone on a product page. Whether a title describes its page and sets it apart from the others is for a person to decide.
How to fix it
Give every page a unique, descriptive <title>, set once in the theme layout template in the form "Page name – Shop name" (for example "Wool Scarf – Example Shop"). Make sure the search, account and error templates output a title too.
WCAG success criteria
- 2.4.2 Page Titled, level A
EN 301 549 clauses
| WCAG criterion | Version 3.2.1 | Version 4.1.1 |
|---|---|---|
| 2.4.2 | 9.2.4.2 | 9.2.4.2 |
Where a recipe fixes it
Shopify: recipe document-title-template
Show the before and after (1)
layout/theme.focused.liquid Changes to layout/theme.focused.liquid: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 11 | 11 | <meta charset="utf-8"> | |
| 12 | 12 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
| 13 | 13 | <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | |
| 14 | Removed | <title></title> | |
| 14 | Added | <title>{{ page_title }}{% unless page_title contains shop.name %} – {{ shop.name }}{% endunless %}</title> | |
| 15 | 15 | <link rel="canonical" href="{{ canonical_url }}"> | |
| 16 | 16 | <!-- Theme assets: //cdn.shopify.com/s/files/1/0712/3456/7890/t/12/assets/ (served locally by the fixture server) --> | |
| 17 | 17 | {% render 'meta-tags' %} |
Shopware: recipe document-title-template
Show the before and after (1)
Resources/views/storefront/layout/meta.html.twig Changes to Resources/views/storefront/layout/meta.html.twig: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 36 | 36 | {% endblock %} | |
| 37 | 37 | ||
| 38 | 38 | {% block layout_head_title %} | |
| 39 | Removed | <title>{{ page.metaTitle }}</title> | |
| 39 | Added | <title>{{ page.metaTitle|default(shop.name) }}</title> | |
| 40 | 40 | {% endblock %} | |
| 41 | 41 | ||
| 42 | 42 | {% block layout_head_stylesheet %} |
WooCommerce: recipe document-title-template
Show the before and after (1)
functions.php Changes to functions.php: 2 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 87 | 87 | if ( is_singular() && metadata_exists( 'post', get_the_ID(), '_kb_seo_title' ) ) { | |
| 88 | 88 | // VALIDISCAN-SEED document-title: the Contact page has the field saved empty, and an empty | |
| 89 | 89 | // string wins over the generated title, so <title> is empty there. | |
| 90 | Removed | return (string) get_post_meta( get_the_ID(), '_kb_seo_title', true ); | |
| 90 | Added | $custom_title = trim( (string) get_post_meta( get_the_ID(), '_kb_seo_title', true ) ); | |
| 91 | Added | return '' !== $custom_title ? $custom_title : $title; | |
| 91 | 92 | } | |
| 92 | 93 | return $title; | |
| 93 | 94 | } |
Magento: recipe document-title-template
Show the before and after (1)
app/design/frontend/Validiscan/fixture/Magento_CatalogSearch/layout/catalogsearch_result_index.xml Changes to app/design/frontend/Validiscan/fixture/Magento_CatalogSearch/layout/catalogsearch_result_index.xml: 0 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 4 | 4 | <!-- VALIDISCAN-SEEDED document-title: a "hide the SEO title" customisation sets an empty page title, | |
| 5 | 5 | so the rendered <title></title> is empty. The visible h1 is set separately by the page.main.title block. | |
| 6 | 6 | Fixed variant: <title>Search results for: 'jacket'</title>. --> | |
| 7 | Removed | <title> </title> | |
| 8 | 7 | </head> | |
| 9 | 8 | <body> | |
| 10 | 9 | <referenceBlock name="search_result_list" template="Magento_Catalog::product/list.phtml"/> |
Start with a free check of your shop.
Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.
Request a free checkOr write to info@validiscan.com.
What the free check covers, and what it leaves out- You send
- your shop’s address
- Pages scanned
- up to 15
- You get
- every failure the scan can prove, with the page and the code
- On your shop
- public pages only; no login, nothing bought, no forms sent
- What it is not
- a full audit