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