Magento SEO: The Technical Setup Guide for 2.4.7 to 2.4.9
Magento SEO, set up path by path: canonical and robots settings, layered navigation rules, hreflang, JSON-LD and a Search Console index-bloat check.

Magento SEO comes down to six things you control: crawl rules, canonical tags, URL settings, meta fields, structured data and page speed. Most of that work happens in admin configuration and theme templates before anyone writes a word of copy. A catalog left at install defaults can hand Google more filter, sort and search URLs than product pages, with nothing on any of them saying which address is the real one. This guide covers Magento 2.4.7 to 2.4.9, on Magento Open Source and Adobe Commerce, one setting at a time: the admin path, the value to choose, and how to check that it worked.
Orange Collar Media builds SEO Suite, an SEO module for Magento sold under the OCM Labs name and one of the Magento extensions we build. Every section below starts with what Magento does on its own, and says plainly where a module helps and where it does not.
What is Magento SEO?
SEO on Magento is the work of getting a catalog crawled efficiently and indexed correctly, so Google spends its attention on products, categories and content pages instead of on URL variants of them. Copy matters too. But copy on a page Google never picks as canonical does nothing.
Magento ships more SEO controls than its reputation suggests. Its gaps sit exactly where large catalogs get into trouble: faceted URLs, multi-language store views and rich results. The table lists what a clean 2.4.x install gives you. A dash in the default column means the value depends on the install, so read it from your own admin before you change anything.
| ADMIN PATH | DEFAULT | WHAT IS MISSING | |
|---|---|---|---|
| PRODUCT AND CATEGORY URL SUFFIX | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | .html | Nothing; the risk is changing it on a live store |
| CATEGORY PATH IN PRODUCT URLS | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | No | Nothing, as long as it stays off |
| 301 WHEN A URL KEY CHANGES | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | Yes | No bulk redirect import, no 410 response |
| CATEGORY/PRODUCT URL REWRITES | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | No | Upgraded stores may hold Yes, and then the rewrite count grows with every category assignment |
| CANONICAL TAG ON CATEGORIES | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | No | Paginated pages point to page one on 2.4.7 (self-canonical from 2.4.8) |
| CANONICAL TAG ON PRODUCTS | Stores > Configuration > Catalog > Catalog > Search Engine Optimization | No | Nothing once it is on |
| CANONICAL TAG ON CMS AND HOME PAGES | None | - | No native option |
| DEFAULT TITLE, PREFIX, SUFFIX, DEFAULT DESCRIPTION | Content > Design > Configuration > (scope) > HTML Head | - | No render-time templates by page type |
| DEFAULT ROBOTS META | Content > Design > Configuration > (scope) > Search Engine Robots | INDEX,FOLLOW | No robots value by page, filter or page type |
| ROBOTS.TXT EDITOR | Content > Design > Configuration > (scope) > Search Engine Robots | - | Nothing; it is a free-text field |
| META AUTO-GENERATION MASKS | Stores > Configuration > Catalog > Catalog > Product Fields Auto-Generation | Built from the product name | Values are written once and never refresh |
| XML SITEMAP | Stores > Configuration > Catalog > XML Sitemap | - | Nothing that blocks you |
| HREFLANG | None | - | No output at all |
| STRUCTURED DATA | Luma product page template | Microdata | No JSON-LD, no BreadcrumbList |
| 410 GONE RESPONSE | None | - | Not available in the admin |
URL handling and redirect history are solid, and the canonical switches exist but ship turned off. Every entry in the right-hand column gets its fix in the section that covers it: a setting, a theme template, a web server rule or a module.
Magento SEO best practices: the short checklist
Run these in order. For the full audit, with a severity and a verify step for every check, use the full store audit checklist.
- Count catalog URLs against Search Console's indexed count before you change any setting (index bloat).
- Leave URL suffixes alone on a live store, and pick them before launch on a new one (URLs).
- Keep Use Categories Path for Product URLs set to No (URLs).
- Give every store view its own crawlable address, through a store code or its own base URL (URLs).
- Keep Create Permanent Redirect for URLs if URL Key Changed set to Yes (URLs).
- Turn on both canonical switches, for products and for categories (canonical tags).
- Decide which filter types earn a real indexable page and which rely on the category canonical alone, and write the decision down as a table (layered navigation).
- Block sort, limit, view-mode and price parameters in robots.txt only if they waste crawl, and only once Google has consolidated them (layered navigation).
- Check that production serves
INDEX,FOLLOWand staging does not (robots). - Generate one XML sitemap per store view and submit each one in Search Console (sitemap).
- Write titles and descriptions by hand for the categories and products that earn traffic (meta titles).
- Add Product and BreadcrumbList JSON-LD to product pages and validate it (structured data).
- Output reciprocal hreflang across every store view that has an equivalent page (hreflang).
- Measure Core Web Vitals from field data, one template at a time (Core Web Vitals).
- Decide how out-of-stock, discontinued and moved URLs behave before the first product goes away (catalog lifecycle).
How do you measure index bloat before changing anything?
Count before you configure. Every section after this one changes what Google crawls, and without a baseline you cannot tell whether a change fixed the problem or moved it somewhere else. The method needs admin access, Search Console and, ideally, your web server access logs.
Step 1: count what should be indexed
Three numbers come from the admin, one set per store view.
- Products. In Catalog > Products, filter Status to Enabled and switch the grid to the store view you are counting. Count every visibility except Not Visible Individually, which has no product page of its own.
- Categories. Count the active categories in Catalog > Categories, or count the category entries in that store view's generated XML sitemap.
- CMS pages. In Content > Elements > Pages, filter Status to Enabled and Store View to the one you are counting.
If the store view's sitemap is current, grep -o "<loc>" sitemap.xml | wc -l gives you a quick cross-check. Add the three admin numbers together. Multiply across store views only where each store view has its own URLs.
Step 2: read what Google has indexed
Open Search Console > Indexing > Pages for the property. Note the indexed count, then work down the "Why pages aren't indexed" table. The filter at the top switches between all known pages, all submitted pages and unsubmitted pages only, and that last view is the part of the index you never asked for.
The reasons that map most directly to Magento URL patterns:
- Duplicate without user-selected canonical usually means filter and sort URLs on a store with the category canonical switch off.
- Alternate page with proper canonical tag means the canonical works, and it is where your filter and sort URLs should collect. Google still spends some crawls on them.
- Crawled - currently not indexed and Discovered - currently not indexed collect thin filter combinations, and sometimes real products Google does not value yet.
- Excluded by 'noindex' tag should hold your search URLs and nothing else.
- Blocked by robots.txt should match your robots.txt rules line for line.
- Not found (404) and Page with redirect show disabled products and changed URL keys.
Check the indexed side as well, for "Indexed, though blocked by robots.txt". Those URLs are the robots.txt trap from the layered navigation section, already sitting in your index.
Step 3: compare the two and find the patterns
Take a hypothetical store with one store view: 4,000 visible products, 300 active categories and 20 enabled CMS pages. That is 4,320 URLs that deserve a place in the index. If the Page indexing report shows 9,000 indexed pages, at least 4,680 of them are something other than the catalog. If it shows 3,100, the problem runs the other way, with at least 1,220 catalog pages missing, and the not-indexed reasons tell you why.
Either way the next step is the same. Export the example URLs from each reason, sort them, and group them by pattern. On Magento the patterns are predictable:
| EXAMPLE | WHERE IT COMES FROM | |
|---|---|---|
| ATTRIBUTE FILTER PARAMETERS | /trail-shoes.html?color=49 | Layered navigation, attribute code plus option ID |
| SORT, DIRECTION, LIMIT AND VIEW MODE | ?product_list_order=price&product_list_dir=desc | Toolbar controls on every category page |
| PRICE RANGES | ?price=50-100 | Price filter in layered navigation |
| INTERNAL SEARCH RESULTS | /catalogsearch/result/?q=trail | Search box, plus anyone who links to a search |
| CATEGORY-PATH PRODUCT URLS | /shoes/trail/trail-runner-2.html | Category/product rewrites, old links |
| STORE SWITCHING AND FRONT CONTROLLER VARIANTS | ?___store=de, /index.php/trail-runner-2.html | Store switcher, rewrites off at some point |
Search Console shows examples, not every URL. Confirm volumes in your access logs, because Googlebot requests per pattern show where crawl activity actually goes:
# Googlebot requests by URL pattern, from a combined-format access log
grep "Googlebot" access.log | grep -c "product_list_order="
grep "Googlebot" access.log | grep -c "/catalogsearch/"
grep "Googlebot" access.log | grep -E -c "[?&]color="User-agent strings are easy to fake. Verify a sample of those IP addresses with a reverse DNS lookup before you trust the totals.
Search Console's lag and sampling matter less than they seem here, because you are looking for URL patterns and those show up in any sample.
Step 4: fix, then recount
Change one pattern at a time where you can, starting with the biggest. Google has to recrawl a URL before it sees a new noindex or canonical, so the report moves slowly. Use URL Inspection on a few example URLs to confirm Google sees the change, and use Validate Fix on the reason in the Page indexing report to ask for a recheck. Then recount against the same three admin numbers, and keep the counts in a dated sheet, because every section below will move them.
How should you set URL suffixes, category paths and rewrites?
Leave working URLs alone. Most Magento 2 SEO trouble with URLs is self-inflicted: a suffix changed on a live store, a category path switched on, or two store views sharing one address. Every setting in this section changes the URL of every product or category it touches, so read each one before you save.
URL suffix
Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Product URL Suffix (catalog/seo/product_url_suffix) and Category URL Suffix (catalog/seo/category_url_suffix). Both default to .html.
Google does not rank a URL higher or lower for ending in .html. On a new build, choose the suffix before launch, drop it then if you prefer extensionless URLs, and never touch it again.
On a live store, keep what you have, even if clean URLs look more current. Changing it rewrites every product and category URL at once, which makes it a migration with a redirect map and a recrawl. Do not assume the admin redirects the old addresses for you. Test a sample with curl on a staging copy first.
Category path in product URLs
Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Use Categories Path for Product URLs (catalog/seo/product_use_categories), default No. Keep it at No.
With Yes, and with category/product rewrites generated, a product assigned to three categories answers at three paths, and category listings link to the category-path version. The URL Google settles on can then change every time a merchandiser re-categorizes a product. Any benefit from a keyword in the path is small next to the cost of several addresses per product. The short URL is stable.
Category/product rewrites
Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Generate "category/product" URL Rewrites (catalog/seo/generate_category_product_rewrites). On a clean install the default is No. A store upgraded from an older release may still hold Yes, so read the value in your own admin before you plan around it.
With Yes, Magento writes one rewrite per category assignment per store view, and those rewrites are what make category-path product URLs resolve even with the path setting off. A big catalog with a deep tree then builds a large url_rewrite table. With No, category-path product URLs do not resolve at all, and the short URL is the only address a product has.
If your store holds Yes and that table has grown large enough to slow product saves and imports, switching to No is reasonable while category paths stay off and the product canonical is on. Switching it to No deletes the existing category/product rewrites from url_rewrite immediately, and they cannot be restored by switching the setting back. Back up the table first. Then grep your access logs and your backlink data for category-path product URLs, because those URLs stop resolving the moment you save.
Store code in URLs
Add Store Code to Urls lives at Stores > Configuration > General > Web > Url Options (web/url/use_store) and defaults to No.
Each store view needs its own crawlable address. If an English and a German store view share one base URL and shoppers switch with a cookie or a ___store= parameter, Google crawls one language and sees the switcher URLs as duplicates. There are two fixes. Set Add Store Code to Urls to Yes, which gives you /en/ and /de/ paths, or give each store view its own base URL at Stores > Configuration > General > Web > Base URLs at store-view scope.
The store code becomes the path segment, so rename a code like default to en at Stores > Settings > All Stores before you switch it on. On a live store this is another migration: every URL changes.
Redirects when a URL key changes
Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Create Permanent Redirect for URLs if URL Key Changed (catalog/seo/save_rewrites_history), default Yes. Keep it at Yes.
The setting pre-ticks a "Create Permanent Redirect for old URL" checkbox in the Search Engine Optimization section of each product and category edit screen. Whoever edits the key can still untick it. When they do, the old URL returns a 404. Put that box on your content team's checklist.
Web server rewrites
Stores > Configuration > General > Web > Search Engine Optimization > Use Web Server Rewrites (web/seo/use_rewrites). Set it to Yes so storefront URLs drop index.php. The index.php variants usually still resolve, so test one with curl -sI; that is one more reason to turn on the canonical switches in the next section.
SEO Suite does not manage URL rewrites or redirects. Everything in this section stays native Magento, whichever module you run.
How do canonical tags work in Magento?
Magento emits a canonical link on product and category pages only when two admin switches are on, and both ship set to No. While they are off, nothing on the page tells Google which of a product's addresses is the real one, so turn both on.
- Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Use Canonical Link Meta Tag For Products (
catalog/seo/product_canonical_tag), default No. Set it to Yes. - Stores > Configuration > Catalog > Catalog > Search Engine Optimization > Use Canonical Link Meta Tag For Categories (
catalog/seo/category_canonical_tag), default No. Set it to Yes.
Clean the full page cache after saving with bin/magento cache:clean full_page, or cached pages keep serving the old head.
Product canonical and category paths
With the product switch on, Magento points the canonical at the product URL without any category path. On a store where Generate "category/product" URL Rewrites is Yes, a product reachable at /trail-runner-2.html and at /shoes/trail/trail-runner-2.html declares the first as canonical on both. That is the behavior you want, and it lets old category-path links keep working without competing with the short URL.
Google treats a canonical as a hint, not a command. It weighs internal links, sitemap entries and redirects alongside the tag, so keep those signals pointing at the same short product URL in navigation, in the sitemap and in links inside CMS content.
Category canonical, filters and pagination
The category switch points a category URL at the bare category address and drops filter and sort parameters, so /trail-shoes.html?color=49&product_list_order=price declares /trail-shoes.html. For filter and sort URLs that is right. Pagination depends on the version. On 2.4.8 and 2.4.9 the category canonical keeps p and drops every other parameter, so /trail-shoes.html?p=2 declares itself, which is what Google's guidance for paginated listings asks for. On 2.4.7, page two declares page one as its canonical.
On 2.4.7 that costs you something: products that only appear on page three and beyond can lose their crawl path when Google folds those pages into page one. There is no admin setting for it. The fix is a small plugin in a custom module that keeps p in the category canonical and drops every other parameter, which is what 2.4.8 does on its own. Check which behavior your store has before you write any code:
curl -sL "https://www.example.com/trail-shoes.html?p=2" | grep -o '<link *rel="canonical"[^>]*>'If the href ends in ?p=2, paginated pages already declare themselves and there is nothing to build.
The 2.4.8 behavior copies whatever p value the request carries, so /trail-shoes.html?p=1 declares itself too and becomes a second copy of page one. Keep every link to page one, in themes, modules and CMS content, pointing at the bare category URL.
CMS and home pages
Native Magento outputs no canonical tag on CMS pages or on the home page. Check whether your home page content also answers at its CMS identifier, such as /home, and whether tracking parameters create indexable copies of landing pages. A self-referencing canonical on CMS pages needs a layout change in your theme or a module.
One canonical per page
Two canonical tags pointing at different URLs give Google conflicting instructions, and Google may ignore both. On Magento it happens when a theme and a module, or two modules, each add one. Check a product, a category, a filtered category and a CMS page:
curl -s https://www.example.com/trail-runner-2.html | grep -o 'rel="canonical"' | wc -lEach should print 1. If you run our module, its robots and canonical handling is documented separately, including how it treats filtered and paginated categories.
How do you configure layered navigation for SEO?
A filtered URL earns a place in the index only when it matches real search demand on its own and shows a set of products meaningfully different from the unfiltered category. Brand often passes that test. A single color inside a category sometimes does. Size, price, sort order and combinations of two or more filters rarely do, and they are where index bloat on a Magento store comes from.
Native Magento builds filter URLs from the attribute code and the option ID: /trail-shoes.html?color=49, /trail-shoes.html?color=49&size=172, and /trail-shoes.html?price=50-100 for a price range. At install defaults each of those pages carries INDEX,FOLLOW and no canonical tag. With the category canonical on, they all canonicalize to the bare category, and for most of them that canonical is the whole fix.
Filter, sort and price-range URLs get the canonical alone: a canonical to the unfiltered category and no noindex. A noindex paired with a canonical that points at another URL sends Google mixed signals. Leave these URLs crawlable so Google can read the canonical and consolidate them.
For a filter worth indexing, build a real page. A subcategory such as "Trail shoes from Brand X", or a CMS landing page, gets its own URL key, title, description, copy and self-canonical, and a filtered URL natively gets none of those. The new page duplicates the filter result on purpose: the filter URL keeps pointing at its parent category, and the new page is the one address built to rank for that demand.
The robots.txt trap
A URL disallowed in robots.txt is never fetched. Google never sees its noindex and never sees its canonical, and it can still index the bare URL from links, without any content. That is how "Indexed, though blocked by robots.txt" ends up in a Page indexing report.
So robots.txt backs up the canonical; it never replaces it. A robots.txt Disallow stays an optional crawl-budget step for parameter patterns that waste crawl: sort, limit, view mode and deep multi-filter combinations. Add it only after Google has consolidated those URLs, or from day one on a store that never had them indexed, because Google cannot read the canonical on a blocked URL.
A starting robots.txt for filter, sort and search URLs
Adapt this to your own attribute codes and paths. It goes in the custom instructions field covered in the next section.
User-agent: *
# Internal search results
Disallow: /catalogsearch/
# Optional crawl-budget lines: add them only once Google has consolidated the matching URLs,
# or from day one on a store that never had them indexed
# Sort, direction, limit and view-mode parameters
Disallow: /*?*product_list_order=
Disallow: /*?*product_list_dir=
Disallow: /*?*product_list_limit=
Disallow: /*?*product_list_mode=
# Price ranges and store switching
Disallow: /*?*price=
Disallow: /*?*___store=
# Session and account paths
Disallow: /checkout/
Disallow: /customer/
Disallow: /wishlist/
Disallow: /catalog/product_compare/
# Pagination (?p=) and single attribute filters stay crawlable
Sitemap: https://www.example.com/sitemap.xmlGoogle reads * as a wildcard, so /*?*product_list_order= matches the parameter anywhere in the query string. Test every line before it ships. Search Console's robots.txt report shows the file Google last fetched, and URL Inspection tells you whether a given URL is blocked.
Meta robots and canonical by filter type
The canonical does the consolidating, for filtered URLs already in the index as well as new ones: Google folds them into the category as it recrawls them and reads the tag. Magento emits that canonical on every filtered URL once the category switch is on, and the Default Robots value of INDEX,FOLLOW lets Google keep crawling those pages and following their product links. Google may set a category canonical aside when a filtered page looks different enough from its category. When that happens on a filter with real demand, give it a real page of its own.
| ROBOTS META | CANONICAL TARGET | CRAWL OR BLOCK | REASON | |
|---|---|---|---|---|
| BRAND, WITH SEARCH DEMAND OF ITS OWN | Index, follow, as a real subcategory or landing page | The new page itself | Crawl | Shoppers search brand plus product type, and a real page carries its own title and copy |
| SINGLE COLOR OR MATERIAL, WITH DEMAND | Index, follow, as a real page; otherwise no noindex | The new page, or the unfiltered category | Crawl | Same test as brand; build the page only where the demand exists |
| SIZE, FIT OR WIDTH | No noindex | Unfiltered category | Crawl | Results swing with stock, so the page changes week to week |
| PRICE RANGE | No noindex | Unfiltered category | Crawl; optional price= block once Google has consolidated them, or from day one if never indexed | Ranges are open-ended and rarely match a search query |
| TWO OR MORE FILTERS COMBINED | No noindex | Unfiltered category | Crawl; optional block for deep combinations once Google has consolidated them, or from day one if never indexed | Combinations multiply far faster than the catalog grows |
| MULTI-SELECT WITHIN ONE ATTRIBUTE | No noindex | Unfiltered category | Crawl; optional block once Google has consolidated them, or from day one if never indexed | Luma's layered navigation takes one value per attribute, so these URLs come from a theme or extension |
| SORT AND DIRECTION | No noindex | Unfiltered category | Crawl; optional block once Google has consolidated them, or from day one if never indexed | Same products in a different order |
| LIMIT AND VIEW MODE | No noindex | Unfiltered category | Crawl; optional block once Google has consolidated them, or from day one if never indexed | Same products at a different page size or layout |
| PAGINATION (P=) | Index, follow | The page itself (native on 2.4.8+ with the category canonical on; needs code on 2.4.7) | Crawl | Products deep in a category need a crawl path |
None of those rows needs a robots plugin: the category canonical and the Default Robots value cover every one. Write the table for your own attribute set before anyone touches robots.txt.
On SEO Suite, the settings sit under Stores > Configuration > OCM Labs > SEO. For canonical-only filters, set Filtered Category Pages to "Use Default (inherit)", which leaves Magento's own robots value in place, and keep Filtered Category Canonical on the unfiltered category. Filtered Category Pages ships set to "noindex, follow", so change it before you rely on the canonical alone. A robots value set on an individual product, category or page still wins.
To verify, curl a filtered or sort URL: the unfiltered category is the rel="canonical", and the robots meta carries no noindex. In Search Console Page indexing, these URLs collect under "Alternate page with proper canonical tag", not "Excluded by 'noindex' tag".
curl -s "https://www.example.com/trail-shoes.html?color=49&product_list_order=price" | grep -i -e 'name="robots"' -e 'rel="canonical"'How do you set up robots.txt, meta robots and the XML sitemap?
All three live in the admin, and two of them are scoped. Check each website and store view as well as the default config.
Where Magento serves robots.txt from
Content > Design > Configuration, then edit the row for the scope you want, then Search Engine Robots > Edit custom instruction of robots.txt File (design/search_engine_robots/custom_instructions). Magento serves that field at /robots.txt for the scope, which lets each website on its own domain carry its own file.
The reset button beside the field fills it with Magento's stock instructions, and that list includes Disallow: /*?. That one line blocks every URL with a query string, pagination included, and hides every filter URL's canonical from Google. Remove it if you follow the table above.
A physical robots.txt in the web root usually wins over the admin value, because the web server returns the file before the request ever reaches Magento. If your admin edits never appear, look for that file. Then check the live result on each domain with curl -s https://www.example.com/robots.txt.
The default robots meta value
Content > Design > Configuration > (scope) > Search Engine Robots > Default Robots (design/search_engine_robots/default_robots), default INDEX,FOLLOW. That value goes on every page in the scope. Production should read INDEX,FOLLOW. Staging should read NOINDEX,NOFOLLOW and also sit behind HTTP authentication, because the meta tag only works on pages Google fetches and a password keeps the staging copy out of reach entirely.
The incident to guard against is a staging database or a locked config value reaching production with NOINDEX,NOFOLLOW still set. If you lock configuration with bin/magento app:config:dump, check which values landed in app/etc/config.php and app/etc/env.php. After every deploy, view the source of one live product page and read the robots tag.
Internal search results
Search results live under /catalogsearch/result/?q=. Keep them out of the index: they duplicate categories, and anyone can create a new one by typing a query and linking to it. If none are indexed yet, the Disallow line in the robots.txt above is enough. If some are, they need noindex first, and Magento has no separate robots setting for them. A change to your theme's search results layout, or a module, has to set it, and the Disallow line follows once the pages drop.
XML sitemap configuration and generation
The settings live at Stores > Configuration > Catalog > XML Sitemap: frequency and priority for categories, products and CMS pages, image options for products, cron generation settings, file limits, and Search Engine Submission Settings > Enable Submission to Robots.txt. Set that last one to Yes and Magento adds a Sitemap: line to the robots.txt it serves.
Google ignores the priority and change frequency values. Do not spend an hour tuning them. It does use lastmod when the dates are accurate.
Generate the files at Marketing > SEO & Search > Site Map. Add one sitemap per store view, each with its own filename and path, then use Save & Generate. Turn on cron generation in the XML Sitemap settings so the files follow the catalog as products come and go, and confirm Magento's cron actually runs on the server. Submit each file in Search Console's Sitemaps report.
Then spot-check the file. Every URL in it should return 200, be canonical and be indexable. A sitemap full of URLs that redirect or canonicalize elsewhere contradicts your own canonical tags.
SEO Suite does not generate XML sitemaps. Magento's own generator does that job, and nothing in this section changes if you install a module.
AI crawler user-agents
This file is also where you decide whether AI crawlers may use your content. Google-Extended is the token Google reads to decide whether your content may be used for its Gemini models, and disallowing it does not remove pages from Google Search. Other AI crawlers publish their own user-agent tokens. Add a User-agent group for each one you want to block, in the same custom instructions field, and recheck the list whenever you review the file.
How should you write meta titles and descriptions in Magento?
Write them by hand for the pages that earn traffic, and let defaults cover the rest. Magento gives you three places to set them and one mechanism that looks like automation but is not.
Fields on the edit screens
Products, categories and CMS pages each have a Search Engine Optimization section on their edit screen, with URL Key, Meta Title, Meta Keywords and Meta Description. Switch the scope selector on a product or category to a store view to write translated values; left at default scope, every store view inherits the default text. A CMS page has no scope selector: a translated version is a separate page assigned to its own store view, and it can reuse the same URL key or take a translated one. When Meta Title is empty on a product, Magento uses the product name as the title.
Google ignores the meta keywords tag. Leave that field empty.
HTML Head defaults, prefix and suffix
Content > Design > Configuration > (scope) > HTML Head holds Default Page Title, Page Title Prefix, Page Title Suffix and Default Meta Description. The prefix and suffix wrap every title in the scope, product pages included. Use the suffix for the store name if you want it at all. Leave the prefix empty, because a prefix pushes the product name to the right, where truncation cuts it.
Leave Default Meta Description empty as well. One sentence stamped on every page that lacks its own description makes those pages look identical in results, while an empty field lets Google build a snippet from the page content. A generic line can beat a random snippet on a page nobody wrote for. The better fix for that page is a description written for it.
Product Fields Auto-Generation masks
Stores > Configuration > Catalog > Catalog > Product Fields Auto-Generation holds masks for Meta Title, Meta Keywords and Meta Description, built from product attribute codes. The stock masks use the product name, and the Meta Description mask adds the product description. The masks fill those fields when someone creates a product in the admin. The result is stored on the product as ordinary text, so it never updates: rename the product or change an attribute the mask used, and the old title stays.
Render-time templates work the other way. They compute the title each time the page renders, from current attribute values, and write nothing back to the product. Native Magento has no render-time templates; some modules, ours among them, add them for products, categories and CMS pages.
Meta title and description guidance by page type
Google cuts titles by pixel width rather than by a character count, and it may rewrite a title or description it judges a poor match for the query. Put the words that matter in the first half of the title, where they survive truncation.
- Product pages. Lead with the product name, then add the attribute shoppers search with, such as model number, material or compatibility, and let the suffix carry the store name if there is room. The description says what the page offers that the next result might not: stock, delivery, or the spec that decides the purchase.
- Category pages. Use the category as shoppers phrase it plus one qualifier, such as "Women's trail running shoes", and keep filter values out of the title. The description names the range and what sets it apart.
- CMS pages. Name the page's job in plain words, such as "Returns and exchanges", and write a description that answers the question the page exists for.
The demo below shows a different way to do the same job: writing a product title and description against a desktop and mobile results preview on the edit screen, with length feedback as you type. Watch the preview more than the score. In SEO Suite the score is guidance for the person writing the page, not a prediction of where it will rank. If you are comparing modules, here is what to look for in an SEO extension for Magento, live content analysis included.
Structured data: what Magento outputs and what valid Product JSON-LD looks like
Native Magento outputs no JSON-LD. Luma's product page template carries schema.org microdata on elements such as the product name, SKU and price, and there is no BreadcrumbList, Organization or WebSite markup. Other themes differ, so view the source of your own product page before you plan anything.
Google reads both formats and recommends JSON-LD. Add it from one template block: it does not depend on theme HTML staying put, and one file is easier to keep correct than attributes scattered across templates.
This is what one product page should emit, for a hypothetical shoe at https://www.example.com/trail-runner-2.html: a Product and a BreadcrumbList in one @graph.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Product",
"@id": "https://www.example.com/trail-runner-2.html#product",
"name": "Trail Runner 2",
"sku": "TR2-BLK-42",
"description": "Lightweight trail running shoe with a rock plate.",
"image": "https://www.example.com/media/catalog/product/t/r/trail-runner-2.jpg",
"url": "https://www.example.com/trail-runner-2.html",
"brand": {
"@type": "Brand",
"name": "Example Outdoor Co."
},
"offers": {
"@type": "Offer",
"url": "https://www.example.com/trail-runner-2.html",
"price": "129.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Trail Shoes",
"item": "https://www.example.com/trail-shoes.html"
},
{
"@type": "ListItem",
"position": 3,
"name": "Trail Runner 2"
}
]
}
]
}Every value must match the visible page. The price is the price the shopper sees, as a number with no currency symbol, and availability switches to https://schema.org/OutOfStock when the stock status does. The url is the canonical URL, never a category-path variant. The breadcrumb's last item can leave out item, because it is the page itself.
Emit one Product node per product page, and emit none when the product has nothing valid to describe. Google needs a name plus at least one of offers, review or aggregateRating, so an empty Product node earns an error rather than partial credit. If Luma's microdata stays in place, the Rich Results Test will report two Product items. Make sure they agree on price and availability.
To validate, paste the URL or the code into Google's Rich Results Test, then into the Schema Markup Validator for anything that is valid schema.org but outside Google's rich result types. After Google recrawls, the Product snippets and Merchant listings reports in Search Console show errors and warnings across the whole catalog. Valid markup makes a page eligible for rich results; Google still decides whether to show them. The full setup, from Product JSON-LD to merchant listing eligibility, is in our guide to Magento rich snippets.
How do you set up hreflang across Magento store views?
Native Magento outputs no hreflang tags. You add them with a template in your theme or with a module. Writing the tags is the easy part; the work is mapping the same product, category or CMS page across store views whose URL keys differ.
Where each store view's locale lives
Stores > Configuration > General > Locale Options > Locale (general/locale/code), set at store-view scope. Magento stores the locale in the form en_US. Hreflang wants an ISO 639-1 language code, optionally followed by a hyphen and an ISO 3166-1 alpha-2 region code, so en_US becomes en-us. Lowercase is the common convention.
| LOCALE | HREFLANG | BASE URL | |
|---|---|---|---|
| ENGLISH, US (EN) | en_US | en-us | https://www.example.com/en/ |
| GERMAN (DE) | de_DE | de-de | https://www.example.com/de/ |
| FALLBACK FOR EVERYONE ELSE | - | x-default | https://www.example.com/en/ |
The x-default decision
The x-default value names the page for visitors whose language matches none of your versions. Point it at a language selector if you have one. Otherwise point it at the store view you would show an unknown visitor, which is usually your largest market. Make that choice once, for the whole site.
What the output looks like
Here are the rendered tags for one product across two store views. The German store view has its own URL key, which is why the mapping has to run on the product ID and never on the URL string:
<link rel="alternate" hreflang="en-us" href="https://www.example.com/en/trail-runner-2.html">
<link rel="alternate" hreflang="de-de" href="https://www.example.com/de/traillaufschuh-2.html">
<link rel="alternate" hreflang="x-default" href="https://www.example.com/en/trail-runner-2.html">Both pages carry the same three lines. Every version lists every version, itself included, and every link must be returned: if the German page lists the English one and the English page does not list it back, Google can ignore the pair. Each href is the canonical, indexable URL of that version.
Leave a store view out of the set when the product is disabled there or not assigned to that website. An alternate that 404s or redirects breaks the set for that page. If one English store view serves every English-speaking country, use plain en rather than inventing region codes it does not serve.
Search Console retired its International Targeting report, so check hreflang yourself: fetch both pages with curl and compare the lines. The SEO Suite hreflang documentation covers how the module formats codes and which pages it leaves without alternates.
Which Magento settings move Core Web Vitals?
Google rates a page experience as good when, at the 75th percentile of page loads, Largest Contentful Paint (LCP) is 2.5 seconds or less, Interaction to Next Paint (INP) is 200 milliseconds or less, and Cumulative Layout Shift (CLS) is 0.1 or less. INP replaced First Input Delay as the responsiveness metric in March 2024. On a JavaScript-heavy theme that change raised the bar, because INP counts the latency of every interaction on the page rather than only the delay before the first one.
LCP: server response and the main image
The LCP element on a product or category page is usually the main product image or a hero banner, and it cannot paint before the HTML arrives. Set Stores > Configuration > Advanced > System > Full Page Cache > Caching Application (system/full_page_cache/caching_application) to Varnish for production; the built-in cache is fine for development.
Then fix the image itself. Serve the main image at the size it displays, in a modern format, and never lazy-load it. Pre-generate resized catalog images with bin/magento catalog:images:resize after imports, so a shopper's request never waits on resizing.
INP: JavaScript on the main thread
INP measures how quickly the page responds to a tap, click or key press, and on Magento the main cost is JavaScript. Luma's front end runs on RequireJS, jQuery and Knockout, and every extension that adds a storefront widget adds to that load. Hyva replaces the stack with Alpine.js and Tailwind CSS, which is why a Hyva storefront ships less JavaScript to the browser.
The admin settings sit at Stores > Configuration > Advanced > Developer. JavaScript Settings holds Merge JavaScript Files (dev/js/merge_files), Enable JavaScript Bundling (dev/js/enable_js_bundling) and Minify JavaScript Files (dev/js/minify_files). CSS Settings holds Merge CSS Files (dev/css/merge_css_files) and Minify CSS Files (dev/css/minify_files). The Developer section only appears in developer mode, so on production set the values from the command line and redeploy static content:
bin/magento config:set dev/js/minify_files 1
bin/magento config:set dev/css/minify_files 1
bin/magento setup:static-content:deploy
bin/magento cache:flushMinify, yes. Native bundling groups modules without regard to which page needs them, so a product page can download code it never runs. Measure INP and LCP on real templates before and after, and turn bundling off again if the numbers get worse.
A move to Hyva is a re-theme, and every extension's storefront output has to be checked against it, so it belongs in a project plan with its own budget. On Luma, the levers within reach are minification, image sizing and removing third-party tags nobody reads the data from.
CLS: reserving space
Layout shift on Magento storefronts tends to come from images without dimensions, sliders and promo bars that render after load, cookie notices that push content down, and swatches that a JavaScript widget draws after the first paint. Give every image width and height attributes. Reserve the slot for anything injected late. Load web fonts with font-display set so text does not jump when the font arrives.
Measure with field data
Judge every fix by field data from real visitors. Search Console's Core Web Vitals report, under Experience, groups URLs by status from real Chrome users, split into mobile and desktop. PageSpeed Insights shows the same Chrome UX Report field data for a URL or the whole origin, above its Lighthouse lab result. Field data covers a rolling 28-day window, so a fix shows up gradually rather than the next morning.
Lighthouse runs under controlled conditions, which makes it the right tool for finding causes, though its scores still move between runs. It is not what Search Console reports. Measure by template, covering home, category, product and search results, because a fix on one template says nothing about the others.
What should happen to out-of-stock, discontinued and changed URLs?
Keep a URL alive while it still serves the visitor, redirect it when a true replacement exists, and let it go with a 404 or 410 when nothing does. Magento handles part of this natively. A 410 is not a native Magento admin option: it needs a web server rule or custom code.
| RESPONSE | HOW TO DO IT IN MAGENTO | THE TRAP | |
|---|---|---|---|
| TEMPORARILY OUT OF STOCK | Keep live, 200 | Leave the product enabled and set its stock status to Out of Stock. Display Out of Stock Products at Stores > Configuration > Catalog > Inventory > Stock Options (cataloginventory/options/show_out_of_stock, default No) controls whether it still shows in listings and search | Disabling the product to hide it turns its URL into a 404; the JSON-LD availability must change to OutOfStock |
| DISCONTINUED, REPLACEMENT EXISTS | 301 to the replacement | Delete the old product, or change its URL key with the "Create Permanent Redirect for old URL" box unticked, then add the custom rewrite; or edit the generated redirect's Target Path in Marketing > SEO & Search > URL Rewrites to the replacement | If Magento reports that the request path already exists, the old product's own rewrite still holds it |
| DISCONTINUED, NO REPLACEMENT | 410, or 404 | Disabling or deleting the product returns a 404. For a 410, add a web server rule, such as an nginx location block that returns 410 for that path | Redirecting every dead product to the home page or a category |
| URL KEY CHANGED | 301 old to new | Automatic while Create Permanent Redirect for URLs if URL Key Changed is Yes and the checkbox on the edit screen stays ticked | Links inside CMS blocks still point at the old URL |
| CATEGORY RESTRUCTURE | 301 old category and product paths | Moving a category or changing its URL key regenerates its rewrites, and with the redirect setting on, the old paths become 301s | Never assume; crawl the old URL list after the change |
| PLATFORM MIGRATION | 301 every old URL with traffic or links to its closest new equivalent | Build and load a redirect map, as below | Chains, loops and blanket redirects to the home page |
Redirecting a discontinued product to its category is right only when the category genuinely serves the same intent. Done across the board, it invites Google to treat those redirects as soft 404s, and the shopper who clicked a specific product lands on a list with no explanation.
The migration redirect map
- Export every old URL that matters: the old sitemap, the Pages tab of Search Console's Performance report, your analytics landing pages, server logs and your backlink data.
- Map each one to its single closest new URL, by SKU for products and by hand for categories and content pages, in a two-column sheet.
- Load the map. The URL Rewrites grid adds custom rewrites one at a time, so at migration scale load them with a script or as a web server redirect map.
- Crawl the old list on launch day. Every old URL should return one 301 to a URL that returns 200, with no chains.
- Submit the new sitemaps in Search Console, and keep the redirects in place.
A loop like this checks step 4 from a plain text list of old URLs:
while read -r url; do
first=$(curl -s -o /dev/null -w "%{http_code}" "$url"); final=$(curl -sL -o /dev/null --max-redirs 10 -w "%{http_code} %{num_redirects} %{url_effective}" "$url"); printf '%s %s %s\n' "$first" "$final" "$url"
done < old-urls.txtA passing line starts 301 200 1.
If you would rather not write and maintain the robots meta plugin for search results yourself, SEO Suite for Magento handles it from its own section at Stores > Configuration > OCM Labs > SEO, along with the filtered category canonical, hreflang and JSON-LD. It costs $250 for Magento Open Source and $500 for Adobe Commerce, and runs on 2.4.7 to 2.4.9.
✦ FAQ
How do you do SEO on Magento?+–
What is the best SEO setup for Magento?+–
Is Magento good for SEO out of the box?+–
What SEO features does Magento offer natively?+–
Which ecommerce platform is best for SEO?+–
Do you need an SEO extension for Magento?+–

Shane Blandford
Founder of Orange Collar Media, a Denver ecommerce agency behind 800+ Magento and Shopify builds. In Magento since version 1.x - building, rescuing, and supporting revenue-critical stores since 2008.
✦ KEEP READING
Related from the Journal.
Magento 2 Rich Snippets: How to Set Up Product Schema That Validates
Magento 2 rich snippets, done by hand: Google's required Product properties, a JSON-LD block mapped to Magento data, and fixes for duplicate microdata.

Magento SEO Checklist for 2.4.x: Severity, Admin Path and a Verify Step for Every Item
A Magento SEO checklist for 2.4.x stores: 50 items, each with a severity, the admin path or bin/magento command, and a way to verify it passes.

What a Magento 2 SEO Extension Should Do, and How to Test One
What a Magento SEO extension should do as you edit: a live 0-100 score, an 18-check list with fix hints, a Google preview, and a buyer's test for demos.
