Skip to main content

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

CounterMeaning
addedNew cross-store pairs written into byte8_seosuite_url
skippedPairs that already exist in the table (idempotency)
missingEntity 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
errorsReal 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-001 to NEW-001 in store 1, the matcher will create a new row for NEW-001's pairs but won't delete the orphaned OLD-001 rows. Run seosuite: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 Individually you 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