medusa-plugin-slug
Gives Medusa products and categories a slug per locale, so a storefront can resolve a localized URL back to its product or category.
Evolve's GraphQL contract exposes a localized slug per product and category,
and the storefront routes on it: /de/produkt/… has to resolve to the same
product as /nl/product/…. The plugin stores those slugs and adds the Store
API route that resolves them.
When to use
Install it in a Medusa backend that serves an Evolve storefront in more than
one language. Stock Medusa has one handle per product for all locales, so a
translated URL could be shown but never resolved, and the shopper gets a 404.
The plugin has no admin screen and no write route. Slugs are written through
its module service, registered as slug, by an import or a seed script.
@evolve-framework/medusa-plugin-bluestone writes a slug for every locale when
this plugin is installed.
Get started
Install the package with the peer dependencies it needs. It comes from the Evolve registry, which needs a one-time sign-in.
pnpm add @evolve-framework/medusa-plugin-slug @medusajs/cli @medusajs/framework @medusajs/medusaRequirements4
node>=20@medusajs/cli2.21.2@medusajs/framework2.21.2@medusajs/medusa2.21.2
For example:
// medusa-config.ts// After adding the plugin, run `npx medusa db:migrate` to create its table.import { defineConfig } from "@medusajs/framework/utils";
module.exports = defineConfig({ // projectConfig, modules and other plugins as before plugins: [{ resolve: "@evolve-framework/medusa-plugin-slug", options: {} }],});Routes
GET /store/slugs answers in two directions. Both need reference, which is
product or product_category.
Resolve a slug to a resource:
GET /store/slugs?reference=product_category&value=broeken&locale=nl-NL
The locale may also come from the x-medusa-locale header: req.locale
resolves the query parameter, then the header, then the store default, the
same as every other translated read. Returns 404 when nothing matches.
List a resource's slugs across locales:
GET /store/slugs?reference=product&reference_id=prod_123
Returns every locale's slug for that resource, which is what builds
alternate-language links on a page. reference_id may be repeated or
comma-separated, so a listing gets the slugs of all its products in one call.
Why this exists
Stock Medusa cannot translate a slug. handle is not among the fields its
translation module will translate, and asking the Admin API to add it is
rejected outright:
product doesn't have the following fields set as translatable: handle
Making it translatable would not have been enough anyway. Translation substitutes values when a row is read, while filtering still runs against the base column. So a translated handle could be displayed per locale but never resolved back to its product, which is the entire job of a slug.
How it works
One row per resource per locale, with the locale as an ordinary column:
| Column | Meaning |
|---|---|
reference |
product or product_category |
reference_id |
the resource's Medusa id |
locale_code |
BCP 47 tag, such as de-DE |
value |
the slug for that resource in that locale |
Two unique indexes, one per direction: (reference, locale_code, value) so a
slug resolves to exactly one resource, and (reference, reference_id, locale_code) so a resource has exactly one slug per locale.
Slugs are independent of titles, so editing a translated title never silently changes a URL.

