@webupro/medusa-power-seo
v0.1.0
Published
SEO scoring for Medusa v2 — Rank Math-style product score, per-check breakdown, SERP preview and a catalogue-wide worst-first list, built on @power-seo/content-analysis.
Maintainers
Readme
@webupro/medusa-power-seo
SEO scoring for Medusa v2, built on @power-seo/content-analysis.
Adds a Rank Math-style score to the product page — a dial, a grouped checklist, a live Google preview — and a catalogue-wide list that answers "where do I start?". Multi-locale shops score each language separately.

Install
npm install @webupro/medusa-power-seo// medusa-config.ts
module.exports = defineConfig({
plugins: [{ resolve: "@webupro/medusa-power-seo", options: {} }],
})Restart the backend. The product page gains an SEO score panel and the sidebar gains an SEO scores page.
Configuration
Configuration is environment variables, not plugin options: this plugin
registers no module (it has no table of its own), and Medusa forwards
plugins[].options to a plugin's modules. Every variable has a default that
produces a working single-locale English install.
| Variable | Default | What it does |
| --- | --- | --- |
| POWER_SEO_STOREFRONT_URL | STOREFRONT_URL, else http://localhost:8000 | Origin of the canonical URL that gets graded |
| POWER_SEO_PRODUCT_PATH | /{locale}/products/{handle} | Canonical path template |
| POWER_SEO_SOURCE_LOCALE | en | The locale whose copy lives on the product row |
| POWER_SEO_LOCALES | (source only) | Extra locales to score, comma-separated |
| POWER_SEO_BRAND_SUFFIX | (none) | Appended to a generated <title>, e.g. \| LIFTED |
| POWER_SEO_ADMIN_LANG | en | Admin copy language — en, hu or sk |
| POWER_SEO_TRANSLATIONS_MODULE | translations | Container key of the module holding locale overlays |
A two-language Slovak shop:
POWER_SEO_STOREFRONT_URL=https://lifted.sk
POWER_SEO_SOURCE_LOCALE=sk
POWER_SEO_LOCALES=hu
POWER_SEO_BRAND_SUFFIX=| LIFTED
POWER_SEO_ADMIN_LANG=huMulti-locale scoring
Each locale ships its own <title> and <meta description>, so each earns its
own score. The source locale is graded from the product row itself. Every other
locale is an overlay read from the shop's translations module, resolved by
container key, over rows of (resource_type, resource_id, locale, field,
value) — the shape MedusaService generates for a Translation model. The
overlay fields are title, description, seo_title, seo_description and
seo_keyphrase.
Resolution order per locale, chosen so the score grades the page that actually ships: the locale's own SEO override → the locale's translated copy → the source locale. A source-locale override never outranks a translation, otherwise a Hungarian page would be graded on copy no Hungarian visitor sees.
A shop with no translations module still works — it scores its source locale, and the locale tabs never render.
What gets graded
The analyser runs 99 checks, but most grade a blog article: author schema, source quality, FAQ and TL;DR sections, citation readiness, freshness. A perfectly good product page scores 44% against the full set, which makes the raw number useless to an operator. This plugin grades the 17 checks a product detail page can actually influence and normalises over the ones that ran.
| Section | Checks | | --- | --- | | Basic SEO | title length and keyphrase, keyphrase position, meta description length and keyphrase, keyphrase in URL, description length | | Keyphrase | length, density, first-paragraph presence, distribution, even distribution | | Readability | paragraph length, sentence length | | Technical | URL length, canonical URL, product images |
Deliberately excluded: the E-E-A-T / AEO / search-intent families
(article-shaped), heading / link / table-of-contents checks (owned by the
storefront template, not by any admin field), image-alt and
image-keyphrase (storefronts render the product title as alt, so it is never
actually missing), the English-wordlist checks transition-words,
word-complexity, title-sentiment, title-power-word and
inclusive-language (the library takes a locale but no check reads it, so
they mis-grade every other language), and text-presence (it duplicates
word-count).
Keyphrase checks are skipped, not failed, until the operator sets a focus keyphrase — grading a product against a phrase nobody chose is noise.
API
| Route | Returns |
| --- | --- |
| GET /admin/seo-score?locale=&q=&limit= | Every published product, worst first, plus count, average, locales and the admin label pack |
| GET /admin/seo-score/:id?locale= | One product with the per-check breakdown and the meta the SERP will show |
Scoring is pure and costs ~3ms per product, so the catalogue is graded per request rather than cached — a stale score is worse than a one-second wait, and there is no invalidation to get wrong. Paging stops at 2000 products.
The focus keyphrase is stored on metadata.seo_keyphrase and is editable in
the source locale only; its translation belongs to the shop's translations
module.
Development
npm install
npm test # unit tests — pure, no database
npm run build # medusa plugin:buildLicence
MIT
