Appearance
Reserved Paths
Reserved paths are per-consumer-platform route reservations that prevent content authors from aliasing a vanity URL onto a path that the consuming frontend routes to app functionality. Without reservations, a content author could create a vanity URL at /cart or /sign-in, and the resulting manifest would redirect shoppers off real application routes — a functional regression.
Reserved paths are stored in the cms_reserved_paths table and managed via this API + the EngageHQ platform-admin UI. Each entry binds a path to a platform_key (matching the same kebab-case convention used by Platform Elements), or to the sentinel '*' for entries that apply to every platform (infrastructure routes like /api and /admin).
When reserved paths apply
| Operation | Enforcement |
|---|---|
POST / PUT /api/v1/content/items with a vanity_urls payload | The union of every platform's active reserved paths is applied — most restrictive. A content author cannot create a path that ANY platform has reserved. |
GET /api/v1/public/vanity-urls?platform_key=... (manifest) | Only the requesting platform's reserved entries plus universal (*) entries are applied. Grandfathered entries (authored before a path was reserved) silently drop out of the manifest for that platform. |
Authentication
All endpoints below require an authenticated admin JWT and are gated to platform super admins (is_super_admin=true in the JWT claims). Regular org admins — even those with full content-management permissions — cannot mutate reserved paths. Reserved paths are platform policy, not org content.
Endpoints
GET /api/v1/content/reserved-paths
List reserved paths, optionally filtered by platform_key or is_active. Results are ordered by platform_key, then path.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
platform_key | string | no | Filter to a single platform. Use * to get universal entries only. |
is_active | boolean | no | Filter to active or inactive entries. |
search | string | no | Case-insensitive substring match against path and description. |
per_page | integer | no | Pagination page size (max 100, default 50). |
Response
json
{
"success": true,
"data": {
"data": [
{
"id": "rpa_...",
"platform_key": "shophero-ecom-v2",
"path": "/cart",
"match_type": "prefix",
"description": "E-commerce cart / cartless list page.",
"is_active": true,
"created_by": "system",
"updated_by": null,
"created_at": "2026-04-16T12:00:00+00:00",
"updated_at": "2026-04-16T12:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 33,
"last_page": 1
}
}
}POST /api/v1/content/reserved-paths
Register a new reserved path.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
platform_key | string | yes | Kebab-case identifier (e.g. shophero-ecom-v2) or * for a universal entry. Must match /^(\*|[a-z0-9]([a-z0-9-]*[a-z0-9])?)$/. |
path | string | yes | Must match /^\/[a-z0-9][a-z0-9\-\/.]*$/. Case-sensitive unique per platform_key. |
match_type | enum | no | prefix (default) or exact. |
description | string | no | Human-readable note — which feature owns this route? |
is_active | boolean | no | Defaults to true. |
Match types
prefix(default): blocks the exact path AND anything starting withpath + '/'. Registering/cartblocks/cartand/cart/checkout.exact: blocks only the literal path. Registering/exact-onlyblocks/exact-onlybut not/exact-only/sub.
Success response
Returns the created entry (same shape as the GET response's data.data[] items).
Error responses
| Status | Condition |
|---|---|
403 | Caller is not a platform super admin. |
422 | Invalid platform_key / path format, or duplicate (platform_key, path). |
GET /api/v1/content/reserved-paths/{reservedPath}
Fetch a single entry by hashkey.
PUT /api/v1/content/reserved-paths/{reservedPath}
Update mutable fields. platform_key and path are immutable — delete and recreate to change them. This keeps audit trails coherent and prevents silent behavior changes for entries other code references by (platform_key, path).
Request body (all optional)
| Field | Type | Description |
|---|---|---|
match_type | enum | prefix or exact. |
description | string | null | Update the admin note. |
is_active | boolean | Toggle enforcement without deleting the row. |
Error responses
| Status | Condition |
|---|---|
403 | Caller is not a platform super admin. |
404 | No reserved path with the given hashkey. |
422 | Attempted to change immutable platform_key or path. Response body includes errors.immutable_fields listing which fields were rejected. |
DELETE /api/v1/content/reserved-paths/{reservedPath}
Hard-delete the entry. Existing vanity URLs that previously collided with this path will reappear in future manifest builds (the manifest re-checks reservations at build time).
Prefer toggling is_active: false via PUT if you want to temporarily disable enforcement while keeping the audit record.
Data model
| Column | Type | Description |
|---|---|---|
id | bigint | Internal primary key. Not exposed in the API — use hashKey (prefix rpa_). |
platform_key | varchar(50) | Kebab-case consumer identifier, or * for universal. |
path | varchar(255) | Reserved path. |
match_type | enum | prefix | exact. |
description | text null | Admin notes. |
is_active | bool | Soft toggle for enforcement. |
created_by | varchar null | Hashkey of the user who created the entry (or 'system' for seeded rows). |
updated_by | varchar null | Hashkey of the user who last updated the entry. |
created_at / updated_at | timestamp | Standard Laravel timestamps. |
Unique constraint: (platform_key, path).
Seeded baseline
On initial deploy of this feature, the ReservedPathSeeder populates the table with ShopHero-owned platforms. Re-run it on dev/staging after schema refreshes or after adding new routes to a platform:
php artisan db:seed --class=ReservedPathSeederThe seeder is idempotent (updateOrCreate keyed on (platform_key, path)) and only touches ShopHero-owned platform_key values. Entries registered by platform admins for other platforms are not affected.
Changelog
Version history
- 2026-04-16 — Initial release. Introduced as a refactor of the hardcoded
VanityUrlService::RESERVED_PATH_PREFIXESconstant. Entries forshophero-ecom-v2and universal infrastructure paths seeded at schema-introduction time via a one-shot data migration; theReservedPathSeederkeeps the list current going forward.