Hreflang auto-matcher
Walks every store's catalog and pairs entities that share an identifier (SKU for products, url_key for categories). For each cross-store pair, writes a hreflang relationship into byte8_seosuite_url so the storefront emits the correct <link rel="alternate"> tags.
When to use it
- Initial setup of hreflang on a multi-store install — instead of manually creating hundreds of URL relationships
- After importing a new store's product catalog — match the new SKUs to existing ones in other stores
- After URL key changes — re-run to refresh stale pair data
CLI
bin/magento seosuite:hreflang:match
[-e product|category|both] # default: both
[-l <limit>] # Inspect at most N entities per store
[--dry-run] # Report what would be added without writing
Recipe — initial bulk match
# Always dry-run first to see what would be added
bin/magento seosuite:hreflang:match --dry-run
# Sample output:
# Matching products by SKU…
# added=1086 skipped=0 missing=11160 errors=6
# Matching categories by url_key…
# added=156 skipped=0 missing=162 errors=0
# Done. Total added=1242, skipped=0, missing=11322, errors=6.
# missing = entity exists in multiple stores but has no URL rewrite in one of them.
# Then run for real
bin/magento seosuite:hreflang:match
Reading the counters
| Counter | Meaning |
|---|---|
added | New cross-store pairs written into byte8_seosuite_url |
skipped | Pairs that already exist in the table (idempotency) |
missing | Entity exists in multiple stores but has no URL rewrite in one of them — expected when a product or category isn't published across every store, not an error |
errors | Real exceptions (URL-finder transport failure, save failure). Each is also logged at WARN level to var/log/system.log with the entity key |
On a fresh catalog, expect missing to dominate — N stores × M products with patchy coverage produces lots of cross-store pairs that can't resolve a URL on one side. Only errors warrants investigation.
Recipe — incremental match (e.g. nightly)
bin/magento seosuite:hreflang:match -l 500
The matcher is idempotent — re-running won't duplicate existing pairs (skipped count rises, added stays accurate to actual new pairs).
How matching works
Products: matched by SKU. SKU is store-independent in Magento — the same SKU on store 1 and store 2 always means the same product. So pairing is unambiguous.
Categories: matched by url_key. Url_key CAN diverge across stores (each store can have its own url_key). The matcher only pairs categories with identical url_keys. If store 1 has a category with url_key=shoes and store 2 has the same category with url_key=schuhe, the matcher won't pair them — you'd need to use the URL Relationship Manager manually.
What gets written
For each pair (entity in store A, entity in store B), the matcher resolves the URL rewrite for both stores and writes:
INSERT INTO byte8_seosuite_url (
request_path, -- e.g. "shoes/acme-runner.html" (store A's URL)
target_path, -- e.g. "shoes/acme-runner.html" (store B's URL — usually identical)
store_id, -- store B
type_id, -- 2 (HREFLANG)
status -- 1 (active)
)
Two pairs become two rows (one for each direction). Bidirectional hreflang is what Google's spec requires.
Per-row Auto-Match in the Hreflang Health grid
In Marketing → SEO Suite → Hreflang Health, rows with code missing_reciprocal or no_pair_found for product/category get an Auto-Match button. Clicking it runs the matcher (across the whole catalog, not just that row) and marks the originating issue as resolved.
This is per-row in UX terms only — the underlying matcher is store-pair scoped, not row scoped. So clicking Auto-Match on a single warning may resolve dozens of related warnings at once.
Bulk Auto-Match-All
The toolbar Auto-Match All button on the Hreflang Health grid runs both matchProducts() and matchCategories() and reports the totals. Same outcome as the CLI's default behaviour.
Idempotency
The matcher loads existing hreflang rows from byte8_seosuite_url once at the start, builds an in-memory set of {request_path, target_store_id} keys, and skips any pair already present. So:
- Running it once: writes everything new
- Running it again: writes nothing (skipped count rises)
- Running it after adding 10 new SKUs: writes only the new pairs
URL rewrite resolution
When the matcher looks up the URL path for an entity in a given store, it filters url_rewrite rows for redirect_type = 0 (canonical only). This skips the legacy 301-redirect rows that Magento auto-creates when a product or category's url_key changes — without the filter the matcher could pick the old slug and write a stale hreflang pair.
Limitations
- Doesn't handle SKU renames — if you rename SKU
OLD-001toNEW-001in store 1, the matcher will create a new row forNEW-001's pairs but won't delete the orphanedOLD-001rows. Runseosuite:delete -e product -s 1,2(from v1) to clean these up. - Categories with diverging url_keys can't be matched — manual URL Relationship entries needed
- Configurable products' children — only the parent SKU is matched; child SKUs are normally not visible individually so this is the correct behaviour, but if you've made child products
Visible Individuallyyou need to handle them separately
Roadmap (v2.9+)
- Pair detection via custom EAV attribute (e.g.
master_sku) for catalogs where SKU diverges across stores - Webhook trigger from product save events to keep pairs current without cron