docs/developer/core-concepts/slugs.mdx
Spree generates SEO-friendly URL slugs for resources like products, categories, and stores. Instead of accessing resources by ID, you can use clean, readable URLs based on resource names.
The Store API accepts slugs or prefixed IDs interchangeably for resource lookups. The Admin API resolves resources by prefixed ID only — slugs are not accepted.
<CodeGroup>// Both work — slug or ID
const product = await client.products.get('spree-tote')
const product = await client.products.get('prod_86Rf07xd4z')
// Categories use permalink slugs
const category = await client.categories.get('clothing/shirts')
// Admin SDK accepts prefixed IDs only — not slugs
const product = await adminClient.products.get('prod_86Rf07xd4z')
# By slug
curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
-H 'X-Spree-API-Key: pk_xxx'
# By ID
curl 'https://api.mystore.com/api/v3/store/products/prod_86Rf07xd4z' \
-H 'X-Spree-API-Key: pk_xxx'
Slugs are automatically generated from resource names:
| Input | Generated Slug |
|---|---|
Spree T-Shirt | spree-t-shirt |
Café & Restaurant | cafe-and-restaurant |
Summer Collection 2025 | summer-collection-2025 |
If a slug already exists, Spree appends the SKU or a unique identifier to ensure uniqueness.
| Resource | Slug Column | Translatable | Hierarchical |
|---|---|---|---|
| Product | slug | Yes | No |
| Category | permalink | Yes | Yes |
| Store | code | No | No |
Category slugs include the full parent path, making them hierarchical:
clothing → "clothing"
clothing/shirts → "clothing/shirts"
clothing/shirts/t-shirts → "clothing/shirts/t-shirts"
When a parent category is renamed, all child permalinks update automatically.
When a slug changes (e.g., a product is renamed), Spree records the old slug in its history table (friendly_id_slugs).
Note: the Store API product and category endpoints look up the current slug only (find_by! on the slug/permalink column) and do not automatically resolve retired slugs — a request using an old slug returns 404. To preserve SEO and avoid broken links, issue HTTP 301 redirects from old slugs to the current one in your storefront or application layer.
Products and categories support localized slugs — a different slug per locale. The locale is resolved from the request's X-Spree-Locale header:
// English
const product = await client.products.get('red-shoes')
// French — locale goes in the third (request options) argument,
// which sends the X-Spree-Locale header
const product = await client.products.get('chaussures-rouges', undefined, {
locale: 'fr',
})
// Admin SDK accepts prefixed IDs only — not slugs
const product = await adminClient.products.get('prod_86Rf07xd4z')
# English
curl 'https://api.mystore.com/api/v3/store/products/red-shoes' \
-H 'X-Spree-API-Key: pk_xxx'
# French
curl 'https://api.mystore.com/api/v3/store/products/chaussures-rouges' \
-H 'X-Spree-API-Key: pk_xxx' \
-H 'X-Spree-Locale: fr'
Slugs are unique within the same locale but can be duplicated across different locales.
Spree prevents certain words from being used as slugs to avoid route conflicts: new, edit, index, login, logout, admin, and others.
client.products.get() slug/ID lookup