How SEO Suite is built - one analyzer, head output, the SEO table, and extension points
Module OCMLabs_Seo, namespace OCMLabs\Seo, Composer package ocmlabs/module-seo, plus the ocmlabs/module-seo-commerce Adobe Commerce licensing metapackage and the ocmlabs/module-seo-hyva Hyva companion (Hyva_OCMLabsSeo). Declarative schema only.
Design rules
- Magento’s fields stay Magento’s. Meta titles, descriptions, and keywords stay in core
meta_title,meta_description, andmeta_keyword. The module stores only what core has no field for. - Never break a save or a page. Save-time scoring runs after the save commits, and any problem is logged and ignored. Storefront output falls back to Magento’s own behavior if a step fails, so a problem in this module can show up as a missing tag but never as an error page.
- One analyzer. The live panel, save-time scoring, dashboard Re-score, the CLI, and cron all call the same analyzer, so every surface shows the same score.
The analyzer
Model\Analysis\Analyzer takes an analysis input (entity type, store, locale, focus and secondary keywords, meta title and description, URL key, name, content HTML, image alt texts) and runs every check that applies to the entity type. Content HTML, including Page Builder markup, is normalized to plain text, sentences, and paragraphs first, skipping script, style, noscript, and template elements.
Checks implement CheckInterface and are registered in the analyzer’s checks argument in etc/di.xml, keyed by ID (keyword_set, keyword_in_title, and so on). A check declares its weight and the entity types it applies to, and returns pass, warn, fail, or skip with a message and fix hint. Other modules can add or remove checks through that di.xml array.
- Keyword checks extend
AbstractKeywordCheck, which skips when no focus keyword is set. readability_fleschandsentence_lengthextendAbstractReadabilityCheck, which skips unless the store locale starts withen_.paragraph_lengthdoes not, and runs on every locale.
Score = (passed weight + half the warned weight) / applicable weight x 100, with skipped and non-applicable checks left out of both sides.
Live analysis
The panel is a Knockout UI component on the product, category, and CMS page forms (the admin is the same on Luma and Hyva stores). It posts the unsaved form values to ocmlabs_seo/analysis/run, debounced, and renders the result. The controller requires OCMLabs_Seo::analyze and never writes anything.
Save, delete, and scoring
| Event | Behavior |
|---|---|
catalog_, catalog_, cms_ | Writes the submitted SEO values for the scope and scores the entity |
catalog_, catalog_, cms_ | Removes the entity’s SEO rows |
ocmlabs_seo_rescore_changed (daily at 03:00, default group) re-scores rows changed since their last score and entities with no score. bin/magento ocmlabs:seo:rescore scores in batches of 500 through lazy proxies, so it does not slow bin/magento start-up. All errors go to var/log/ocmlabs_seo.log, a dedicated Monolog handler.
Head output
The module writes the head tags after core has set its own:
- Category, CMS, home, and search pages are handled by an observer on
layout_generate_blocks_after. - Product pages are handled by a plugin on
Magento\Catalog\Helper\Product\View::prepareAndRender, because core rewrites the product’s meta title, meta description, and canonical after that event. On product pages the module also replaces core’sog:*block with a price-only version so Open Graph tags are not repeated; with the module off, core’s block is untouched.
With canonical management on, existing rel="canonical" links, including ones added by layout, are removed before the module adds its own.
Structured data
JSON-LD is assembled from a schema pool of builders (product, breadcrumb, organization, website) registered in di.xml on Model\Schema\SchemaPool, and rendered as one application/ld+json script. Values are JSON-encoded with script-closing sequences neutralized.
Caching
Everything the module adds to the storefront is part of the full-page-cached HTML, built from data already on the page. It adds no per-request database reads outside page generation and no cache tags of its own.
When a save or the Set Robots mass action changes a storefront-visible value (robots, canonical URL, social title, description, or image), the module cleans the entity’s existing core cache tag (cat_p_<id>, cat_c_<id>, or cms_p_<id>) through Magento’s standard clean_cache_by_tags event, so Varnish is purged as well. Core purges that tag when the entity is saved, but the module’s row is written afterwards, and the mass action never saves the entity, so the module cleans it again. Score-only updates never clean the cache. A failed clean is logged and the save carries on.
Database
| Table | Purpose |
|---|---|
ocmlabs_ | One row per entity type, entity ID, and store ID (store 0 for the default scope): focus keyword, secondary keywords (JSON array, up to 4), score, the analysis check list, grid flags, robots and canonical overrides, og_, og_, og_, scored_, updated_. Unique on (entity_. |
Store-view rows override the default row field by field; an empty store-view field inherits. CMS pages have only the default row.
Configuration access
All settings are read through Model\Config, a typed reader over ScopeConfigInterface. The General switches and the x-default store view are default-scope settings; everything else can vary by website and store view.
Admin and ACL
The dashboard sits under Marketing > SEO & Search (Magento_Backend::marketing_seo). ACL resources sit under Magento_Backend::marketing in OCMLabs_Seo::main: ::dashboard (with child ::dashboard_manage for mass actions), ::analyze, and ::config. All admin and storefront strings are in i18n/en_US.csv.
Uninstall
bin/magento module:uninstall OCMLabs_Seo --remove-data drops ocmlabs_seo_entity, deletes the ocmlabs_seo/* configuration rows, and removes the nightly job’s timestamp. Products, categories, and CMS pages are untouched.