Skip to content

medusa-plugin-slug

@evolve-framework/medusa-plugin-slug · v1.0.0

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

  1. 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/medusa
    Requirements4
    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.